Remove transactions queue 1027 (#1125)
Remove `PendingSignRequests` queue from the sign module. This closes #1027 by removing the pending sign requests queue dependency from the SendTransaction, SignMessage and Recover.
This commit is contained in:
parent
ebc77374b2
commit
4afd9e6c6c
150
api/backend.go
150
api/backend.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
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/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
gethnode "github.com/ethereum/go-ethereum/node"
|
gethnode "github.com/ethereum/go-ethereum/node"
|
||||||
|
|
||||||
|
@ -19,7 +20,6 @@ import (
|
||||||
"github.com/status-im/status-go/rpc"
|
"github.com/status-im/status-go/rpc"
|
||||||
"github.com/status-im/status-go/services/personal"
|
"github.com/status-im/status-go/services/personal"
|
||||||
"github.com/status-im/status-go/services/rpcfilters"
|
"github.com/status-im/status-go/services/rpcfilters"
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
"github.com/status-im/status-go/signal"
|
"github.com/status-im/status-go/signal"
|
||||||
"github.com/status-im/status-go/transactions"
|
"github.com/status-im/status-go/transactions"
|
||||||
)
|
)
|
||||||
|
@ -34,21 +34,22 @@ var (
|
||||||
ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities")
|
ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities")
|
||||||
// ErrWhisperIdentityInjectionFailure injecting whisper identities has failed.
|
// ErrWhisperIdentityInjectionFailure injecting whisper identities has failed.
|
||||||
ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper")
|
ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper")
|
||||||
|
// ErrUnsupportedRPCMethod is for methods not supported by the RPC interface
|
||||||
|
ErrUnsupportedRPCMethod = errors.New("method is unsupported by RPC interface")
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusBackend implements Status.im service
|
// StatusBackend implements Status.im service
|
||||||
type StatusBackend struct {
|
type StatusBackend struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
statusNode *node.StatusNode
|
statusNode *node.StatusNode
|
||||||
pendingSignRequests *sign.PendingRequests
|
personalAPI *personal.PublicAPI
|
||||||
personalAPI *personal.PublicAPI
|
rpcFilters *rpcfilters.Service
|
||||||
rpcFilters *rpcfilters.Service
|
accountManager *account.Manager
|
||||||
accountManager *account.Manager
|
transactor *transactions.Transactor
|
||||||
transactor *transactions.Transactor
|
newNotification fcm.NotificationConstructor
|
||||||
newNotification fcm.NotificationConstructor
|
connectionState connectionState
|
||||||
connectionState connectionState
|
appState appState
|
||||||
appState appState
|
log log.Logger
|
||||||
log log.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStatusBackend create a new NewStatusBackend instance
|
// NewStatusBackend create a new NewStatusBackend instance
|
||||||
|
@ -56,22 +57,20 @@ func NewStatusBackend() *StatusBackend {
|
||||||
defer log.Info("Status backend initialized")
|
defer log.Info("Status backend initialized")
|
||||||
|
|
||||||
statusNode := node.New()
|
statusNode := node.New()
|
||||||
pendingSignRequests := sign.NewPendingRequests()
|
|
||||||
accountManager := account.NewManager(statusNode)
|
accountManager := account.NewManager(statusNode)
|
||||||
transactor := transactions.NewTransactor(pendingSignRequests)
|
transactor := transactions.NewTransactor()
|
||||||
personalAPI := personal.NewAPI(pendingSignRequests)
|
personalAPI := personal.NewAPI()
|
||||||
notificationManager := fcm.NewNotification(fcmServerKey)
|
notificationManager := fcm.NewNotification(fcmServerKey)
|
||||||
rpcFilters := rpcfilters.New(statusNode)
|
rpcFilters := rpcfilters.New(statusNode)
|
||||||
|
|
||||||
return &StatusBackend{
|
return &StatusBackend{
|
||||||
pendingSignRequests: pendingSignRequests,
|
statusNode: statusNode,
|
||||||
statusNode: statusNode,
|
accountManager: accountManager,
|
||||||
accountManager: accountManager,
|
transactor: transactor,
|
||||||
transactor: transactor,
|
personalAPI: personalAPI,
|
||||||
personalAPI: personalAPI,
|
rpcFilters: rpcFilters,
|
||||||
rpcFilters: rpcFilters,
|
newNotification: notificationManager,
|
||||||
newNotification: notificationManager,
|
log: log.New("package", "status-go/api.StatusBackend"),
|
||||||
log: log.New("package", "status-go/api.StatusBackend"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,11 +89,6 @@ func (b *StatusBackend) Transactor() *transactions.Transactor {
|
||||||
return b.transactor
|
return b.transactor
|
||||||
}
|
}
|
||||||
|
|
||||||
// PendingSignRequests returns reference to a list of current sign requests
|
|
||||||
func (b *StatusBackend) PendingSignRequests() *sign.PendingRequests {
|
|
||||||
return b.pendingSignRequests
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNodeRunning confirm that node is running
|
// IsNodeRunning confirm that node is running
|
||||||
func (b *StatusBackend) IsNodeRunning() bool {
|
func (b *StatusBackend) IsNodeRunning() bool {
|
||||||
return b.statusNode.IsRunning()
|
return b.statusNode.IsRunning()
|
||||||
|
@ -225,12 +219,36 @@ func (b *StatusBackend) CallPrivateRPC(inputJSON string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTransaction creates a new transaction and waits until it's complete.
|
// SendTransaction creates a new transaction and waits until it's complete.
|
||||||
func (b *StatusBackend) SendTransaction(ctx context.Context, args transactions.SendTxArgs) (hash gethcommon.Hash, err error) {
|
func (b *StatusBackend) SendTransaction(sendArgs transactions.SendTxArgs, password string) (hash gethcommon.Hash, err error) {
|
||||||
transactionHash, err := b.transactor.SendTransaction(ctx, args)
|
verifiedAccount, err := b.getVerifiedAccount(password)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
go b.rpcFilters.TriggerTransactionSentToUpstreamEvent(transactionHash)
|
return hash, err
|
||||||
}
|
}
|
||||||
return transactionHash, err
|
|
||||||
|
hash, err = b.transactor.SendTransaction(sendArgs, verifiedAccount)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go b.rpcFilters.TriggerTransactionSentToUpstreamEvent(hash)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignMessage checks the pwd vs the selected account and passes on the signParams
|
||||||
|
// to personalAPI for message signature
|
||||||
|
func (b *StatusBackend) SignMessage(rpcParams personal.SignParams) (hexutil.Bytes, error) {
|
||||||
|
verifiedAccount, err := b.getVerifiedAccount(rpcParams.Password)
|
||||||
|
if err != nil {
|
||||||
|
return hexutil.Bytes{}, err
|
||||||
|
}
|
||||||
|
return b.personalAPI.Sign(rpcParams, verifiedAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover calls the personalAPI to return address associated with the private
|
||||||
|
// key that was used to calculate the signature in the message
|
||||||
|
func (b *StatusBackend) Recover(rpcParams personal.RecoverParams) (gethcommon.Address, error) {
|
||||||
|
return b.personalAPI.Recover(rpcParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedExtKey, error) {
|
func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedExtKey, error) {
|
||||||
|
@ -248,46 +266,6 @@ func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedEx
|
||||||
return selectedAccount, nil
|
return selectedAccount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApproveSignRequest instructs backend to complete sending of a given transaction.
|
|
||||||
func (b *StatusBackend) ApproveSignRequest(id, password string) sign.Result {
|
|
||||||
return b.pendingSignRequests.Approve(id, password, nil, b.getVerifiedAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveSignRequestWithArgs instructs backend to complete sending of a given transaction.
|
|
||||||
// gas and gasPrice will be overrided with the given values before signing the
|
|
||||||
// transaction.
|
|
||||||
func (b *StatusBackend) ApproveSignRequestWithArgs(id, password string, gas, gasPrice int64) sign.Result {
|
|
||||||
args := prepareTxArgs(gas, gasPrice)
|
|
||||||
return b.pendingSignRequests.Approve(id, password, &args, b.getVerifiedAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveSignRequests instructs backend to complete sending of multiple transactions
|
|
||||||
func (b *StatusBackend) ApproveSignRequests(ids []string, password string) map[string]sign.Result {
|
|
||||||
results := make(map[string]sign.Result)
|
|
||||||
for _, txID := range ids {
|
|
||||||
results[txID] = b.ApproveSignRequest(txID, password)
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscardSignRequest discards a given transaction from transaction queue
|
|
||||||
func (b *StatusBackend) DiscardSignRequest(id string) error {
|
|
||||||
return b.pendingSignRequests.Discard(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscardSignRequests discards given multiple transactions from transaction queue
|
|
||||||
func (b *StatusBackend) DiscardSignRequests(ids []string) map[string]error {
|
|
||||||
results := make(map[string]error)
|
|
||||||
for _, txID := range ids {
|
|
||||||
err := b.DiscardSignRequest(txID)
|
|
||||||
if err != nil {
|
|
||||||
results[txID] = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerHandlers attaches Status callback handlers to running node
|
// registerHandlers attaches Status callback handlers to running node
|
||||||
func (b *StatusBackend) registerHandlers() error {
|
func (b *StatusBackend) registerHandlers() error {
|
||||||
var clients []*rpc.Client
|
var clients []*rpc.Client
|
||||||
|
@ -312,30 +290,18 @@ func (b *StatusBackend) registerHandlers() error {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
client.RegisterHandler(
|
client.RegisterHandler(params.SendTransactionMethodName, unsupportedMethodHandler)
|
||||||
params.SendTransactionMethodName,
|
client.RegisterHandler(params.PersonalSignMethodName, unsupportedMethodHandler)
|
||||||
func(ctx context.Context, rpcParams ...interface{}) (interface{}, error) {
|
client.RegisterHandler(params.PersonalRecoverMethodName, unsupportedMethodHandler)
|
||||||
txArgs, err := transactions.RPCCalltoSendTxArgs(rpcParams...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hash, err := b.SendTransaction(ctx, txArgs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash.Hex(), err
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
client.RegisterHandler(params.PersonalSignMethodName, b.personalAPI.Sign)
|
|
||||||
client.RegisterHandler(params.PersonalRecoverMethodName, b.personalAPI.Recover)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unsupportedMethodHandler(ctx context.Context, rpcParams ...interface{}) (interface{}, error) {
|
||||||
|
return nil, ErrUnsupportedRPCMethod
|
||||||
|
}
|
||||||
|
|
||||||
// ConnectionChange handles network state changes logic.
|
// ConnectionChange handles network state changes logic.
|
||||||
func (b *StatusBackend) ConnectionChange(typ string, expensive bool) {
|
func (b *StatusBackend) ConnectionChange(typ string, expensive bool) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
|
|
|
@ -2,12 +2,10 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/status-im/status-go/node"
|
"github.com/status-im/status-go/node"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/rpc"
|
"github.com/status-im/status-go/rpc"
|
||||||
|
@ -108,12 +106,6 @@ func TestBackendGettersConcurrently(t *testing.T) {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
assert.NotNil(t, backend.PendingSignRequests())
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
assert.True(t, backend.IsNodeRunning())
|
assert.True(t, backend.IsNodeRunning())
|
||||||
|
@ -284,58 +276,6 @@ func TestAppStateChange(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareTxArgs(t *testing.T) {
|
|
||||||
var flagtests = []struct {
|
|
||||||
description string
|
|
||||||
gas int64
|
|
||||||
gasPrice int64
|
|
||||||
expectedGas *hexutil.Uint64
|
|
||||||
expectedGasPrice *hexutil.Big
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
description: "Empty gas and gas price",
|
|
||||||
gas: 0,
|
|
||||||
gasPrice: 0,
|
|
||||||
expectedGas: nil,
|
|
||||||
expectedGasPrice: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Non empty gas and gas price",
|
|
||||||
gas: 1,
|
|
||||||
gasPrice: 2,
|
|
||||||
expectedGas: func() *hexutil.Uint64 {
|
|
||||||
x := hexutil.Uint64(1)
|
|
||||||
return &x
|
|
||||||
}(),
|
|
||||||
expectedGasPrice: (*hexutil.Big)(big.NewInt(2)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Empty gas price",
|
|
||||||
gas: 1,
|
|
||||||
gasPrice: 0,
|
|
||||||
expectedGas: func() *hexutil.Uint64 {
|
|
||||||
x := hexutil.Uint64(1)
|
|
||||||
return &x
|
|
||||||
}(),
|
|
||||||
expectedGasPrice: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Empty gas",
|
|
||||||
gas: 0,
|
|
||||||
gasPrice: 2,
|
|
||||||
expectedGas: nil,
|
|
||||||
expectedGasPrice: (*hexutil.Big)(big.NewInt(2)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range flagtests {
|
|
||||||
t.Run(tt.description, func(t *testing.T) {
|
|
||||||
args := prepareTxArgs(tt.gas, tt.gasPrice)
|
|
||||||
assert.Equal(t, tt.expectedGas, args.Gas)
|
|
||||||
assert.Equal(t, tt.expectedGasPrice, args.GasPrice)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBlockedRPCMethods(t *testing.T) {
|
func TestBlockedRPCMethods(t *testing.T) {
|
||||||
backend := NewStatusBackend()
|
backend := NewStatusBackend()
|
||||||
err := backend.StartNode(¶ms.NodeConfig{})
|
err := backend.StartNode(¶ms.NodeConfig{})
|
||||||
|
@ -352,4 +292,4 @@ func TestBlockedRPCMethods(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(adam): add concurrent tests for: SendTransaction, ApproveSignRequest, DiscardSignRequest
|
// TODO(adam): add concurrent tests for: SendTransaction
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
)
|
|
||||||
|
|
||||||
// prepareTxArgs given gas and gasPrice will prepare a valid sign.TxArgs.
|
|
||||||
func prepareTxArgs(gas, gasPrice int64) (args sign.TxArgs) {
|
|
||||||
if gas > 0 {
|
|
||||||
g := hexutil.Uint64(gas)
|
|
||||||
args.Gas = &g
|
|
||||||
}
|
|
||||||
if gasPrice > 0 {
|
|
||||||
gp := (*hexutil.Big)(big.NewInt(gasPrice))
|
|
||||||
args.GasPrice = gp
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -166,23 +166,3 @@ func (cs *commandSet) SelectAccount(address, password string) error {
|
||||||
func (cs *commandSet) Logout() error {
|
func (cs *commandSet) Logout() error {
|
||||||
return cs.statusBackend.Logout()
|
return cs.statusBackend.Logout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApproveSignRequest instructs API to complete sending of a given transaction.
|
|
||||||
func (cs *commandSet) ApproveSignRequest(id, password string) (string, error) {
|
|
||||||
result := cs.statusBackend.ApproveSignRequest(id, password)
|
|
||||||
if result.Error != nil {
|
|
||||||
return "", result.Error
|
|
||||||
}
|
|
||||||
return result.Response.Hex(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApproveSignRequest instructs API to complete sending of a given transaction.
|
|
||||||
// gas and gasPrice will be overrided with the given values before signing the
|
|
||||||
// transaction.
|
|
||||||
func (cs *commandSet) ApproveSignRequestWithArgs(id, password string, gas, gasPrice int64) (string, error) {
|
|
||||||
result := cs.statusBackend.ApproveSignRequestWithArgs(id, password, gas, gasPrice)
|
|
||||||
if result.Error != nil {
|
|
||||||
return "", result.Error
|
|
||||||
}
|
|
||||||
return result.Response.Hex(), nil
|
|
||||||
}
|
|
||||||
|
|
166
lib/library.go
166
lib/library.go
|
@ -13,8 +13,9 @@ import (
|
||||||
"github.com/status-im/status-go/logutils"
|
"github.com/status-im/status-go/logutils"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/profiling"
|
"github.com/status-im/status-go/profiling"
|
||||||
"github.com/status-im/status-go/sign"
|
"github.com/status-im/status-go/services/personal"
|
||||||
"github.com/status-im/status-go/signal"
|
"github.com/status-im/status-go/signal"
|
||||||
|
"github.com/status-im/status-go/transactions"
|
||||||
"gopkg.in/go-playground/validator.v9"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -210,143 +211,46 @@ func Logout() *C.char {
|
||||||
return makeJSONResponse(err)
|
return makeJSONResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//ApproveSignRequestWithArgs instructs backend to complete sending of a given transaction.
|
// SignMessage unmarshals rpc params {data, address, password} and passes
|
||||||
// gas and gasPrice will be overrided with the given values before signing the
|
// them onto backend.SignMessage
|
||||||
// transaction.
|
//export SignMessage
|
||||||
//export ApproveSignRequestWithArgs
|
func SignMessage(rpcParams *C.char) *C.char {
|
||||||
func ApproveSignRequestWithArgs(id, password *C.char, gas, gasPrice C.longlong) *C.char {
|
var params personal.SignParams
|
||||||
result := statusBackend.ApproveSignRequestWithArgs(C.GoString(id), C.GoString(password), int64(gas), int64(gasPrice))
|
err := json.Unmarshal([]byte(C.GoString(rpcParams)), ¶ms)
|
||||||
|
if err != nil {
|
||||||
return prepareApproveSignRequestResponse(result, id)
|
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
|
||||||
|
}
|
||||||
|
result, err := statusBackend.SignMessage(params)
|
||||||
|
return C.CString(prepareJSONResponse(result.String(), err))
|
||||||
}
|
}
|
||||||
|
|
||||||
//ApproveSignRequest instructs backend to complete sending of a given transaction.
|
// Recover unmarshals rpc params {signDataString, signedData} and passes
|
||||||
//export ApproveSignRequest
|
// them onto backend.
|
||||||
func ApproveSignRequest(id, password *C.char) *C.char {
|
//export Recover
|
||||||
result := statusBackend.ApproveSignRequest(C.GoString(id), C.GoString(password))
|
func Recover(rpcParams *C.char) *C.char {
|
||||||
|
var params personal.RecoverParams
|
||||||
return prepareApproveSignRequestResponse(result, id)
|
err := json.Unmarshal([]byte(C.GoString(rpcParams)), ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
|
||||||
|
}
|
||||||
|
addr, err := statusBackend.Recover(params)
|
||||||
|
return C.CString(prepareJSONResponse(addr.String(), err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareApproveSignRequestResponse based on a sign.Result prepares the binding
|
// SendTransaction converts RPC args and calls backend.SendTransaction
|
||||||
// response.
|
//export SendTransaction
|
||||||
func prepareApproveSignRequestResponse(result sign.Result, id *C.char) *C.char {
|
func SendTransaction(txArgsJSON, password *C.char) *C.char {
|
||||||
errString := ""
|
var params transactions.SendTxArgs
|
||||||
if result.Error != nil {
|
err := json.Unmarshal([]byte(C.GoString(txArgsJSON)), ¶ms)
|
||||||
fmt.Fprintln(os.Stderr, result.Error)
|
|
||||||
errString = result.Error.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
out := SignRequestResult{
|
|
||||||
ID: C.GoString(id),
|
|
||||||
Hash: result.Response.Hex(),
|
|
||||||
Error: errString,
|
|
||||||
}
|
|
||||||
outBytes, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to marshal ApproveSignRequest output", "error", err)
|
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
|
||||||
return makeJSONResponse(err)
|
|
||||||
}
|
}
|
||||||
|
hash, err := statusBackend.SendTransaction(params, C.GoString(password))
|
||||||
return C.CString(string(outBytes))
|
code := codeUnknown
|
||||||
}
|
if c, ok := errToCodeMap[err]; ok {
|
||||||
|
code = c
|
||||||
//ApproveSignRequests instructs backend to complete sending of multiple transactions
|
|
||||||
//export ApproveSignRequests
|
|
||||||
func ApproveSignRequests(ids, password *C.char) *C.char {
|
|
||||||
out := SignRequestsResult{}
|
|
||||||
out.Results = make(map[string]SignRequestResult)
|
|
||||||
|
|
||||||
parsedIDs, err := ParseJSONArray(C.GoString(ids))
|
|
||||||
if err != nil {
|
|
||||||
out.Results["none"] = SignRequestResult{
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
txIDs := make([]string, len(parsedIDs))
|
|
||||||
for i, id := range parsedIDs {
|
|
||||||
txIDs[i] = id
|
|
||||||
}
|
|
||||||
|
|
||||||
results := statusBackend.ApproveSignRequests(txIDs, C.GoString(password))
|
|
||||||
for txID, result := range results {
|
|
||||||
txResult := SignRequestResult{
|
|
||||||
ID: txID,
|
|
||||||
Hash: result.Response.Hex(),
|
|
||||||
}
|
|
||||||
if result.Error != nil {
|
|
||||||
txResult.Error = result.Error.Error()
|
|
||||||
}
|
|
||||||
out.Results[txID] = txResult
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return C.CString(prepareJSONResponseWithCode(hash.String(), err, code))
|
||||||
outBytes, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to marshal ApproveSignRequests output", "error", err)
|
|
||||||
return makeJSONResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return C.CString(string(outBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
//DiscardSignRequest discards a given transaction from transaction queue
|
|
||||||
//export DiscardSignRequest
|
|
||||||
func DiscardSignRequest(id *C.char) *C.char {
|
|
||||||
err := statusBackend.DiscardSignRequest(C.GoString(id))
|
|
||||||
|
|
||||||
errString := ""
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
errString = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
out := DiscardSignRequestResult{
|
|
||||||
ID: C.GoString(id),
|
|
||||||
Error: errString,
|
|
||||||
}
|
|
||||||
outBytes, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("failed to marshal DiscardSignRequest output", "error", err)
|
|
||||||
return makeJSONResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return C.CString(string(outBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
//DiscardSignRequests discards given multiple transactions from transaction queue
|
|
||||||
//export DiscardSignRequests
|
|
||||||
func DiscardSignRequests(ids *C.char) *C.char {
|
|
||||||
out := DiscardSignRequestsResult{}
|
|
||||||
out.Results = make(map[string]DiscardSignRequestResult)
|
|
||||||
|
|
||||||
parsedIDs, err := ParseJSONArray(C.GoString(ids))
|
|
||||||
if err != nil {
|
|
||||||
out.Results["none"] = DiscardSignRequestResult{
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
txIDs := make([]string, len(parsedIDs))
|
|
||||||
for i, id := range parsedIDs {
|
|
||||||
txIDs[i] = id
|
|
||||||
}
|
|
||||||
|
|
||||||
results := statusBackend.DiscardSignRequests(txIDs)
|
|
||||||
for txID, err := range results {
|
|
||||||
out.Results[txID] = DiscardSignRequestResult{
|
|
||||||
ID: txID,
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outBytes, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to marshal DiscardSignRequests output", "error", err)
|
|
||||||
return makeJSONResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return C.CString(string(outBytes))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//StartCPUProfile runs pprof for cpu
|
//StartCPUProfile runs pprof for cpu
|
||||||
|
|
|
@ -11,7 +11,6 @@ package main
|
||||||
|
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -21,19 +20,19 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
gethparams "github.com/ethereum/go-ethereum/params"
|
gethparams "github.com/ethereum/go-ethereum/params"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/status-im/status-go/account"
|
"github.com/status-im/status-go/account"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
"github.com/status-im/status-go/signal"
|
"github.com/status-im/status-go/signal"
|
||||||
. "github.com/status-im/status-go/t/utils" //nolint: golint
|
. "github.com/status-im/status-go/t/utils" //nolint: golint
|
||||||
"github.com/status-im/status-go/transactions"
|
"github.com/status-im/status-go/transactions"
|
||||||
|
@ -45,7 +44,7 @@ const initJS = `
|
||||||
};`
|
};`
|
||||||
|
|
||||||
var (
|
var (
|
||||||
zeroHash = sign.EmptyResponse.Hex()
|
zeroHash = gethcommon.Hash{}
|
||||||
testChainDir string
|
testChainDir string
|
||||||
nodeConfigJSON string
|
nodeConfigJSON string
|
||||||
)
|
)
|
||||||
|
@ -127,20 +126,16 @@ func testExportedAPI(t *testing.T, done chan struct{}) {
|
||||||
testAccountLogout,
|
testAccountLogout,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"complete single queued transaction",
|
"send transaction",
|
||||||
testCompleteTransaction,
|
testSendTransaction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"test complete multiple queued transactions",
|
"send transaction with invalid password",
|
||||||
testCompleteMultipleQueuedTransactions,
|
testSendTransactionInvalidPassword,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"discard single queued transaction",
|
"failed single transaction",
|
||||||
testDiscardTransaction,
|
testFailedTransaction,
|
||||||
},
|
|
||||||
{
|
|
||||||
"test discard multiple queued transactions",
|
|
||||||
testDiscardMultipleQueuedTransactions,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +238,7 @@ func testResetChainData(t *testing.T) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||||
testCompleteTransaction(t)
|
testSendTransaction(t)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -350,7 +345,7 @@ func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo
|
||||||
}
|
}
|
||||||
|
|
||||||
// additionally, let's complete transaction (just to make sure that node lives through pause/resume w/o issues)
|
// additionally, let's complete transaction (just to make sure that node lives through pause/resume w/o issues)
|
||||||
testCompleteTransaction(t)
|
testSendTransaction(t)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -776,9 +771,12 @@ func testAccountLogout(t *testing.T) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCompleteTransaction(t *testing.T) bool {
|
type jsonrpcAnyResponse struct {
|
||||||
signRequests := statusBackend.PendingSignRequests()
|
Result json.RawMessage `json:"result"`
|
||||||
|
jsonrpcErrorResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSendTransaction(t *testing.T) bool {
|
||||||
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
// log into account from which transactions will be sent
|
||||||
|
@ -787,487 +785,109 @@ func testCompleteTransaction(t *testing.T) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure you panic if transaction complete doesn't return
|
args, err := json.Marshal(transactions.SendTxArgs{
|
||||||
queuedTxCompleted := make(chan struct{}, 1)
|
|
||||||
abortPanic := make(chan struct{}, 1)
|
|
||||||
PanicAfter(10*time.Second, abortPanic, "testCompleteTransaction")
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var txHash = ""
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var envelope signal.Envelope
|
|
||||||
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
|
|
||||||
t.Errorf("cannot unmarshal event's JSON: %s. Error %q", jsonEvent, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
|
|
||||||
|
|
||||||
completeTxResponse := SignRequestResult{}
|
|
||||||
rawResponse := ApproveSignRequest(C.CString(event["id"].(string)), C.CString(TestConfig.Account1.Password))
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &completeTxResponse); err != nil {
|
|
||||||
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if completeTxResponse.Error != "" {
|
|
||||||
t.Errorf("cannot complete queued transaction[%v]: %v", event["id"], completeTxResponse.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
txHash = completeTxResponse.Hash
|
|
||||||
|
|
||||||
t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txHash)
|
|
||||||
abortPanic <- struct{}{} // so that timeout is aborted
|
|
||||||
queuedTxCompleted <- struct{}{}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// this call blocks, up until Complete Transaction is called
|
|
||||||
txCheckHash, err := statusBackend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed to SendTransaction: %s", err)
|
t.Errorf("failed to marshal errors: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
rawResult := SendTransaction(C.CString(string(args)), C.CString(TestConfig.Account1.Password))
|
||||||
|
|
||||||
<-queuedTxCompleted // make sure that complete transaction handler completes its magic, before we proceed
|
var result jsonrpcAnyResponse
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(rawResult)), &result); err != nil {
|
||||||
if txHash != txCheckHash.Hex() {
|
t.Errorf("failed to unmarshal rawResult '%s': %v", C.GoString(rawResult), err)
|
||||||
t.Errorf("Transaction hash returned from SendTransaction is invalid: expected %s, got %s",
|
|
||||||
txCheckHash.Hex(), txHash)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if result.Error.Message != "" {
|
||||||
if reflect.DeepEqual(txCheckHash, gethcommon.Hash{}) {
|
t.Errorf("failed to send transaction: %v", result.Error)
|
||||||
t.Error("Test failed: transaction was never queued or completed")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
hash := gethcommon.BytesToHash(result.Result)
|
||||||
if signRequests.Count() != 0 {
|
if reflect.DeepEqual(hash, gethcommon.Hash{}) {
|
||||||
t.Error("tx queue must be empty at this point")
|
t.Errorf("response hash empty: %s", hash.Hex())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCompleteMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyclo
|
func testSendTransactionInvalidPassword(t *testing.T) bool {
|
||||||
signRequests := statusBackend.PendingSignRequests()
|
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
// log into account from which transactions will be sent
|
||||||
if err := statusBackend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
|
if err := statusBackend.SelectAccount(
|
||||||
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
|
TestConfig.Account1.Address,
|
||||||
|
TestConfig.Account1.Password,
|
||||||
|
); err != nil {
|
||||||
|
t.Errorf("cannot select account: %v. Error %q", TestConfig.Account1.Address, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure you panic if transaction complete doesn't return
|
args, err := json.Marshal(transactions.SendTxArgs{
|
||||||
testTxCount := 3
|
|
||||||
txIDs := make(chan string, testTxCount)
|
|
||||||
allTestTxCompleted := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var txID string
|
|
||||||
var envelope signal.Envelope
|
|
||||||
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
|
|
||||||
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
txID = event["id"].(string)
|
|
||||||
t.Logf("transaction queued (will be completed in a single call, once aggregated): {id: %s}\n", txID)
|
|
||||||
|
|
||||||
txIDs <- txID
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
|
|
||||||
sendTx := func() {
|
|
||||||
txHashCheck, err := statusBackend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error thrown: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
|
|
||||||
t.Error("transaction returned empty hash")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for transactions, and complete them in a single call
|
|
||||||
completeTxs := func(txIDStrings string) {
|
|
||||||
var parsedIDs []string
|
|
||||||
if err := json.Unmarshal([]byte(txIDStrings), &parsedIDs); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedIDs = append(parsedIDs, "invalid-tx-id")
|
|
||||||
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
|
|
||||||
|
|
||||||
// complete
|
|
||||||
resultsString := ApproveSignRequests(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password))
|
|
||||||
resultsStruct := SignRequestsResult{}
|
|
||||||
if err := json.Unmarshal([]byte(C.GoString(resultsString)), &resultsStruct); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results := resultsStruct.Results
|
|
||||||
|
|
||||||
if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error != sign.ErrSignReqNotFound.Error() {
|
|
||||||
t.Errorf("cannot complete txs: %v", results)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for txID, txResult := range results {
|
|
||||||
if txID != txResult.ID {
|
|
||||||
t.Errorf("tx id not set in result: expected id is %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if txResult.Error != "" && txID != "invalid-tx-id" {
|
|
||||||
t.Errorf("invalid error for %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if txResult.Hash == zeroHash && txID != "invalid-tx-id" {
|
|
||||||
t.Errorf("invalid hash (expected non empty hash): %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if txResult.Hash != zeroHash {
|
|
||||||
t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txResult.Hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
|
|
||||||
for _, txID := range parsedIDs {
|
|
||||||
if signRequests.Has(string(txID)) {
|
|
||||||
t.Errorf("txqueue should not have test tx at this point (it should be completed): %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
var txIDStrings []string
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
txIDStrings = append(txIDStrings, <-txIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
txIDJSON, _ := json.Marshal(txIDStrings)
|
|
||||||
completeTxs(string(txIDJSON))
|
|
||||||
allTestTxCompleted <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// send multiple transactions
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
go sendTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-allTestTxCompleted:
|
|
||||||
// pass
|
|
||||||
case <-time.After(20 * time.Second):
|
|
||||||
t.Error("test timed out")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if signRequests.Count() != 0 {
|
|
||||||
t.Error("tx queue must be empty at this point")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo
|
|
||||||
signRequests := statusBackend.PendingSignRequests()
|
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
|
||||||
if err := statusBackend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
|
|
||||||
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure you panic if transaction complete doesn't return
|
|
||||||
completeQueuedTransaction := make(chan struct{}, 1)
|
|
||||||
PanicAfter(20*time.Second, completeQueuedTransaction, "testDiscardTransaction")
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var txID string
|
|
||||||
txFailedEventCalled := make(chan struct{})
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var envelope signal.Envelope
|
|
||||||
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
|
|
||||||
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
txID = event["id"].(string)
|
|
||||||
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
|
|
||||||
|
|
||||||
if !signRequests.Has(string(txID)) {
|
|
||||||
t.Errorf("txqueue should still have test tx: %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// discard
|
|
||||||
discardResponse := DiscardSignRequestResult{}
|
|
||||||
rawResponse := DiscardSignRequest(C.CString(txID))
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &discardResponse); err != nil {
|
|
||||||
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if discardResponse.Error != "" {
|
|
||||||
t.Errorf("cannot discard tx: %v", discardResponse.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// try completing discarded transaction
|
|
||||||
err := statusBackend.ApproveSignRequest(string(txID), TestConfig.Account1.Password).Error
|
|
||||||
if err != sign.ErrSignReqNotFound {
|
|
||||||
t.Error("expects tx not found, but call to CompleteTransaction succeeded")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if signRequests.Has(string(txID)) {
|
|
||||||
t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
completeQueuedTransaction <- struct{}{} // so that timeout is aborted
|
|
||||||
}
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestFailed {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
t.Logf("transaction return event received: %+v\n", event)
|
|
||||||
|
|
||||||
receivedErrMessage := event["error_message"].(string)
|
|
||||||
expectedErrMessage := sign.ErrSignReqDiscarded.Error()
|
|
||||||
if receivedErrMessage != expectedErrMessage {
|
|
||||||
t.Errorf("unexpected error message received: got %v", receivedErrMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
receivedErrCode := event["error_code"].(string)
|
|
||||||
if receivedErrCode != strconv.Itoa(sign.SignRequestDiscardedErrorCode) {
|
|
||||||
t.Errorf("unexpected error code received: got %v", receivedErrCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
close(txFailedEventCalled)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// this call blocks, and should return when DiscardQueuedTransaction() is called
|
|
||||||
txHashCheck, err := statusBackend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
select {
|
t.Errorf("failed to marshal errors: %v", err)
|
||||||
case <-txFailedEventCalled:
|
|
||||||
case <-time.After(time.Second * 10):
|
|
||||||
t.Error("expected tx failure signal is not received")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
rawResult := SendTransaction(C.CString(string(args)), C.CString("invalid password"))
|
||||||
|
|
||||||
if err != sign.ErrSignReqDiscarded {
|
var result jsonrpcAnyResponse
|
||||||
t.Errorf("expected error not thrown: %v", err)
|
if err := json.Unmarshal([]byte(C.GoString(rawResult)), &result); err != nil {
|
||||||
|
t.Errorf("failed to unmarshal rawResult '%s': %v", C.GoString(rawResult), err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if result.Error.Message != keystore.ErrDecrypt.Error() {
|
||||||
if !reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
|
t.Errorf("invalid result: %q", result)
|
||||||
t.Error("transaction returned hash, while it shouldn't")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if signRequests.Count() != 0 {
|
|
||||||
t.Error("tx queue must be empty at this point")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyclo
|
func testFailedTransaction(t *testing.T) bool {
|
||||||
signRequests := statusBackend.PendingSignRequests()
|
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
// log into wrong account in order to get selectedAccount error
|
||||||
if err := statusBackend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
|
if err := statusBackend.SelectAccount(TestConfig.Account2.Address, TestConfig.Account2.Password); err != nil {
|
||||||
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
|
t.Errorf("cannot select account: %v. Error %q", TestConfig.Account1.Address, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure you panic if transaction complete doesn't return
|
args, err := json.Marshal(transactions.SendTxArgs{
|
||||||
testTxCount := 3
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
txIDs := make(chan string, testTxCount)
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
|
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
||||||
var testTxDiscarded sync.WaitGroup
|
|
||||||
testTxDiscarded.Add(testTxCount)
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var txID string
|
|
||||||
var envelope signal.Envelope
|
|
||||||
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
|
|
||||||
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
txID = event["id"].(string)
|
|
||||||
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
|
|
||||||
|
|
||||||
if !signRequests.Has(string(txID)) {
|
|
||||||
t.Errorf("txqueue should still have test tx: %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
txIDs <- txID
|
|
||||||
}
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestFailed {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
|
|
||||||
|
|
||||||
receivedErrMessage := event["error_message"].(string)
|
|
||||||
expectedErrMessage := sign.ErrSignReqDiscarded.Error()
|
|
||||||
if receivedErrMessage != expectedErrMessage {
|
|
||||||
t.Errorf("unexpected error message received: got %v", receivedErrMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
receivedErrCode := event["error_code"].(string)
|
|
||||||
if receivedErrCode != strconv.Itoa(sign.SignRequestDiscardedErrorCode) {
|
|
||||||
t.Errorf("unexpected error code received: got %v", receivedErrCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
testTxDiscarded.Done()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
|
t.Errorf("failed to marshal errors: %v", err)
|
||||||
sendTx := func() {
|
return false
|
||||||
txHashCheck, err := statusBackend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
|
||||||
})
|
|
||||||
if err != sign.ErrSignReqDiscarded {
|
|
||||||
t.Errorf("expected error not thrown: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
|
|
||||||
t.Error("transaction returned hash, while it shouldn't")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
rawResult := SendTransaction(C.CString(string(args)), C.CString(TestConfig.Account1.Password))
|
||||||
|
|
||||||
// wait for transactions, and discard immediately
|
var result jsonrpcAnyResponse
|
||||||
discardTxs := func(txIDStrings string) {
|
if err := json.Unmarshal([]byte(C.GoString(rawResult)), &result); err != nil {
|
||||||
var parsedIDs []string
|
t.Errorf("failed to unmarshal rawResult '%s': %v", C.GoString(rawResult), err)
|
||||||
if err := json.Unmarshal([]byte(txIDStrings), &parsedIDs); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedIDs = append(parsedIDs, "invalid-tx-id")
|
|
||||||
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
|
|
||||||
|
|
||||||
// discard
|
|
||||||
discardResultsString := DiscardSignRequests(C.CString(string(updatedTxIDStrings)))
|
|
||||||
discardResultsStruct := DiscardSignRequestsResult{}
|
|
||||||
if err := json.Unmarshal([]byte(C.GoString(discardResultsString)), &discardResultsStruct); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
discardResults := discardResultsStruct.Results
|
|
||||||
|
|
||||||
if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error != sign.ErrSignReqNotFound.Error() {
|
|
||||||
t.Errorf("cannot discard txs: %v", discardResults)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// try completing discarded transaction
|
|
||||||
completeResultsString := ApproveSignRequests(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password))
|
|
||||||
completeResultsStruct := SignRequestsResult{}
|
|
||||||
if err := json.Unmarshal([]byte(C.GoString(completeResultsString)), &completeResultsStruct); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completeResults := completeResultsStruct.Results
|
|
||||||
|
|
||||||
if len(completeResults) != (testTxCount + 1) {
|
|
||||||
t.Error("unexpected number of errors (call to ApproveSignRequest should not succeed)")
|
|
||||||
}
|
|
||||||
for txID, txResult := range completeResults {
|
|
||||||
if txID != txResult.ID {
|
|
||||||
t.Errorf("tx id not set in result: expected id is %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if txResult.Error != sign.ErrSignReqNotFound.Error() {
|
|
||||||
t.Errorf("invalid error for %s", txResult.Hash)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if txResult.Hash != zeroHash {
|
|
||||||
t.Errorf("invalid hash (expected zero): '%s'", txResult.Hash)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
|
|
||||||
for _, txID := range parsedIDs {
|
|
||||||
if signRequests.Has(string(txID)) {
|
|
||||||
t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
var txIDStrings []string
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
txIDStrings = append(txIDStrings, <-txIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
txIDJSON, _ := json.Marshal(txIDStrings)
|
|
||||||
discardTxs(string(txIDJSON))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// send multiple transactions
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
go sendTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() { testTxDiscarded.Wait(); close(done) }()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
// pass
|
|
||||||
case <-time.After(20 * time.Second):
|
|
||||||
t.Error("test timed out")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if signRequests.Count() != 0 {
|
if result.Error.Message != transactions.ErrInvalidTxSender.Error() {
|
||||||
t.Error("tx queue must be empty at this point")
|
t.Errorf("expected error to be ErrInvalidTxSender, got %s", result.Error.Message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Result != nil {
|
||||||
|
t.Errorf("expected result to be nil")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTestNode(t *testing.T) <-chan struct{} {
|
func startTestNode(t *testing.T) <-chan struct{} {
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
|
"github.com/status-im/status-go/account"
|
||||||
|
"github.com/status-im/status-go/transactions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
codeUnknown int = iota
|
||||||
|
// special codes
|
||||||
|
codeFailedParseResponse
|
||||||
|
codeFailedParseParams
|
||||||
|
// account related codes
|
||||||
|
codeErrNoAccountSelected
|
||||||
|
codeErrInvalidTxSender
|
||||||
|
codeErrDecrypt
|
||||||
|
)
|
||||||
|
|
||||||
|
var errToCodeMap = map[error]int{
|
||||||
|
account.ErrNoAccountSelected: codeErrNoAccountSelected,
|
||||||
|
transactions.ErrInvalidTxSender: codeErrInvalidTxSender,
|
||||||
|
keystore.ErrDecrypt: codeErrDecrypt,
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonrpcSuccessfulResponse struct {
|
||||||
|
Result interface{} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonrpcErrorResponse struct {
|
||||||
|
Error jsonError `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonError struct {
|
||||||
|
Code int `json:"code,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareJSONResponse(result interface{}, err error) string {
|
||||||
|
code := codeUnknown
|
||||||
|
if c, ok := errToCodeMap[err]; ok {
|
||||||
|
code = c
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareJSONResponseWithCode(result, err, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareJSONResponseWithCode(result interface{}, err error, code int) string {
|
||||||
|
if err != nil {
|
||||||
|
errResponse := jsonrpcErrorResponse{
|
||||||
|
Error: jsonError{Code: code, Message: err.Error()},
|
||||||
|
}
|
||||||
|
response, _ := json.Marshal(&errResponse)
|
||||||
|
return string(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(jsonrpcSuccessfulResponse{result})
|
||||||
|
if err != nil {
|
||||||
|
return prepareJSONResponseWithCode(nil, err, codeFailedParseResponse)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nonJSON struct{}
|
||||||
|
|
||||||
|
func (*nonJSON) MarshalJSON() ([]byte, error) {
|
||||||
|
return nil, errors.New("invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareJSONResponseErrorWithResult(t *testing.T) {
|
||||||
|
data := prepareJSONResponse("0x123", nil)
|
||||||
|
require.Equal(t, `{"result":"0x123"}`, data)
|
||||||
|
|
||||||
|
data = prepareJSONResponse(&nonJSON{}, nil)
|
||||||
|
require.Contains(t, data, `{"error":{"code":1,"message":`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareJSONResponseErrorWithError(t *testing.T) {
|
||||||
|
data := prepareJSONResponse("0x123", errors.New("some error"))
|
||||||
|
require.Contains(t, data, `{"error":{"message":"some error"}}`)
|
||||||
|
}
|
23
lib/types.go
23
lib/types.go
|
@ -75,26 +75,3 @@ type NotifyResult struct {
|
||||||
Status bool `json:"status"`
|
Status bool `json:"status"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignRequestResult is a JSON returned from transaction complete function (used in exposed method)
|
|
||||||
type SignRequestResult struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignRequestsResult is list of results from CompleteTransactions() (used in exposed method)
|
|
||||||
type SignRequestsResult struct {
|
|
||||||
Results map[string]SignRequestResult `json:"results"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscardSignRequestResult is a JSON returned from transaction discard function
|
|
||||||
type DiscardSignRequestResult struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscardSignRequestsResult is a list of results from DiscardTransactions()
|
|
||||||
type DiscardSignRequestsResult struct {
|
|
||||||
Results map[string]DiscardSignRequestResult `json:"results"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,53 +6,42 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/status-im/status-go/account"
|
"github.com/status-im/status-go/account"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/rpc"
|
"github.com/status-im/status-go/rpc"
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrInvalidPersonalSignAccount is returned when the account passed to
|
// ErrInvalidPersonalSignAccount is returned when the account passed to
|
||||||
// personal_sign isn't equal to the currently selected account.
|
// personal_sign isn't equal to the currently selected account.
|
||||||
ErrInvalidPersonalSignAccount = errors.New("invalid account as only the selected one can generate a signature")
|
ErrInvalidPersonalSignAccount = errors.New("invalid account as only the selected one can generate a signature")
|
||||||
|
|
||||||
// ErrSignInvalidNumberOfParameters is returned when the number of parameters for personal_sign
|
|
||||||
// is not valid.
|
|
||||||
ErrSignInvalidNumberOfParameters = errors.New("invalid number of parameters for personal_sign (2 or 3 expected)")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
// SignParams required to sign messages
|
||||||
Data interface{} `json:"data"`
|
type SignParams struct {
|
||||||
Address string `json:"account"`
|
Data interface{} `json:"data"`
|
||||||
|
Address string `json:"account"`
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMetadata(rpcParams []interface{}) (*metadata, error) {
|
// RecoverParams are for calling `personal_ecRecover`
|
||||||
// personal_sign can be called with the following parameters
|
type RecoverParams struct {
|
||||||
// 1) data to sign
|
Message string `json:"message"`
|
||||||
// 2) account
|
Signature string `json:"signature"`
|
||||||
// 3) (optional) password
|
|
||||||
// here, we always ignore (3) because we send a confirmation for the password to UI
|
|
||||||
if len(rpcParams) < 2 || len(rpcParams) > 3 {
|
|
||||||
return nil, ErrSignInvalidNumberOfParameters
|
|
||||||
}
|
|
||||||
data := rpcParams[0]
|
|
||||||
address := rpcParams[1].(string)
|
|
||||||
|
|
||||||
return &metadata{data, address}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicAPI represents a set of APIs from the `web3.personal` namespace.
|
// PublicAPI represents a set of APIs from the `web3.personal` namespace.
|
||||||
type PublicAPI struct {
|
type PublicAPI struct {
|
||||||
pendingSignRequests *sign.PendingRequests
|
rpcClient *rpc.Client
|
||||||
rpcClient *rpc.Client
|
rpcTimeout time.Duration
|
||||||
rpcTimeout time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAPI creates an instance of the personal API.
|
// NewAPI creates an instance of the personal API.
|
||||||
func NewAPI(pendingSignRequests *sign.PendingRequests) *PublicAPI {
|
func NewAPI() *PublicAPI {
|
||||||
return &PublicAPI{
|
return &PublicAPI{
|
||||||
pendingSignRequests: pendingSignRequests,
|
rpcTimeout: 300 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,59 +52,32 @@ func (api *PublicAPI) SetRPC(rpcClient *rpc.Client, timeout time.Duration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recover is an implementation of `personal_ecRecover` or `web3.personal.ecRecover` API
|
// Recover is an implementation of `personal_ecRecover` or `web3.personal.ecRecover` API
|
||||||
func (api *PublicAPI) Recover(context context.Context, rpcParams ...interface{}) (interface{}, error) {
|
func (api *PublicAPI) Recover(rpcParams RecoverParams) (addr common.Address, err error) {
|
||||||
var response interface{}
|
ctx, cancel := context.WithTimeout(context.Background(), api.rpcTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err = api.rpcClient.CallContextIgnoringLocalHandlers(
|
||||||
|
ctx,
|
||||||
|
&addr,
|
||||||
|
params.PersonalRecoverMethodName,
|
||||||
|
rpcParams.Message, rpcParams.Signature)
|
||||||
|
|
||||||
err := api.rpcClient.CallContextIgnoringLocalHandlers(
|
return
|
||||||
context, &response, params.PersonalRecoverMethodName, rpcParams...)
|
|
||||||
|
|
||||||
return response, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign is an implementation of `personal_sign` or `web3.personal.sign` API
|
// Sign is an implementation of `personal_sign` or `web3.personal.sign` API
|
||||||
func (api *PublicAPI) Sign(context context.Context, rpcParams ...interface{}) (interface{}, error) {
|
func (api *PublicAPI) Sign(rpcParams SignParams, verifiedAccount *account.SelectedExtKey) (result hexutil.Bytes, err error) {
|
||||||
metadata, err := newMetadata(rpcParams)
|
if !strings.EqualFold(rpcParams.Address, verifiedAccount.Address.Hex()) {
|
||||||
if err != nil {
|
err = ErrInvalidPersonalSignAccount
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req, err := api.pendingSignRequests.Add(context, params.PersonalSignMethodName, metadata, api.completeFunc(context, *metadata))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := api.pendingSignRequests.Wait(req.ID, api.rpcTimeout)
|
|
||||||
return result.Response, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *PublicAPI) completeFunc(context context.Context, metadata metadata) sign.CompleteFunc {
|
|
||||||
return func(acc *account.SelectedExtKey, password string, signArgs *sign.TxArgs) (response sign.Response, err error) {
|
|
||||||
response = sign.EmptyResponse
|
|
||||||
|
|
||||||
err = api.validateAccount(metadata, acc)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = api.rpcClient.CallContextIgnoringLocalHandlers(
|
|
||||||
context,
|
|
||||||
&response,
|
|
||||||
params.PersonalSignMethodName,
|
|
||||||
metadata.Data, metadata.Address, password)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), api.rpcTimeout)
|
||||||
// make sure that only account which created the tx can complete it
|
defer cancel()
|
||||||
func (api *PublicAPI) validateAccount(metadata metadata, selectedAccount *account.SelectedExtKey) error {
|
err = api.rpcClient.CallContextIgnoringLocalHandlers(
|
||||||
if selectedAccount == nil {
|
ctx,
|
||||||
return account.ErrNoAccountSelected
|
&result,
|
||||||
}
|
params.PersonalSignMethodName,
|
||||||
|
rpcParams.Data, rpcParams.Address, rpcParams.Password)
|
||||||
// case-insensitive string comparison
|
|
||||||
if !strings.EqualFold(metadata.Address, selectedAccount.Address.Hex()) {
|
return
|
||||||
return ErrInvalidPersonalSignAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
# sign
|
|
||||||
`sign` package represents the API and signals for sending and receiving
|
|
||||||
signature request to and from our API user.
|
|
||||||
|
|
||||||
When a method is called that requires an additional signature confirmation from
|
|
||||||
a user (like, a transaction), it gets it's sign request.
|
|
||||||
|
|
||||||
Client of the API is then nofified of the sign request.
|
|
||||||
|
|
||||||
Client has a chance to approve the sign request (by providing a valid password)
|
|
||||||
or to discard it. When the request is approved, the locked functinality is
|
|
||||||
executed.
|
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
package sign
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
"github.com/status-im/status-go/account"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
//ErrSignReqNotFound - error sign request hash not found
|
|
||||||
ErrSignReqNotFound = errors.New("sign request not found")
|
|
||||||
//ErrSignReqInProgress - error sign request is in progress
|
|
||||||
ErrSignReqInProgress = errors.New("sign request is in progress")
|
|
||||||
//ErrSignReqTimedOut - error sign request sending timed out
|
|
||||||
ErrSignReqTimedOut = errors.New("sign request sending timed out")
|
|
||||||
//ErrSignReqDiscarded - error sign request discarded
|
|
||||||
ErrSignReqDiscarded = errors.New("sign request has been discarded")
|
|
||||||
)
|
|
||||||
|
|
||||||
// TransientError means that the sign request won't be removed from the list of
|
|
||||||
// pending if it happens. There are a few built-in transient errors, and this
|
|
||||||
// struct can be used to wrap any error to be transient.
|
|
||||||
type TransientError struct {
|
|
||||||
Reason error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the string representation of the underlying error.
|
|
||||||
func (e TransientError) Error() string {
|
|
||||||
return e.Reason.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTransientError wraps an error into a TransientError structure.
|
|
||||||
func NewTransientError(reason error) TransientError {
|
|
||||||
return TransientError{reason}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove from queue on any error (except for transient ones) and propagate
|
|
||||||
// defined as map[string]bool because errors from ethclient returned wrapped as jsonError
|
|
||||||
var transientErrs = map[string]bool{
|
|
||||||
keystore.ErrDecrypt.Error(): true, // wrong password
|
|
||||||
account.ErrNoAccountSelected.Error(): true, // account not selected
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTransient(err error) bool {
|
|
||||||
_, ok := err.(TransientError)
|
|
||||||
if ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_, transient := transientErrs[err.Error()]
|
|
||||||
return transient
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
package sign
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
"github.com/status-im/status-go/signal"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// MessageIDKey is a key for message ID
|
|
||||||
// This ID is required to track from which chat a given send transaction request is coming.
|
|
||||||
MessageIDKey = contextKey("message_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey string // in order to make sure that our context key does not collide with keys from other packages
|
|
||||||
|
|
||||||
// messageIDFromContext returns message id from context (if exists)
|
|
||||||
func messageIDFromContext(ctx context.Context) string {
|
|
||||||
if ctx == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if messageID, ok := ctx.Value(MessageIDKey).(string); ok {
|
|
||||||
return messageID
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendSignRequestAdded sends a signal when a sign request is added.
|
|
||||||
func SendSignRequestAdded(request *Request) {
|
|
||||||
signal.SendSignRequestAdded(
|
|
||||||
signal.PendingRequestEvent{
|
|
||||||
ID: request.ID,
|
|
||||||
Args: request.Meta,
|
|
||||||
Method: request.Method,
|
|
||||||
MessageID: messageIDFromContext(request.context),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendSignRequestFailed sends a signal only if error had happened
|
|
||||||
func SendSignRequestFailed(request *Request, err error) {
|
|
||||||
signal.SendSignRequestFailed(
|
|
||||||
signal.PendingRequestEvent{
|
|
||||||
ID: request.ID,
|
|
||||||
Args: request.Meta,
|
|
||||||
Method: request.Method,
|
|
||||||
MessageID: messageIDFromContext(request.context),
|
|
||||||
},
|
|
||||||
err, sendTransactionErrorCode(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// SignRequestNoErrorCode is sent when no error occurred.
|
|
||||||
SignRequestNoErrorCode = iota
|
|
||||||
// SignRequestDefaultErrorCode is every case when there is no special tx return code.
|
|
||||||
SignRequestDefaultErrorCode
|
|
||||||
// SignRequestPasswordErrorCode is sent when account failed verification.
|
|
||||||
SignRequestPasswordErrorCode
|
|
||||||
// SignRequestTimeoutErrorCode is sent when tx is timed out.
|
|
||||||
SignRequestTimeoutErrorCode
|
|
||||||
// SignRequestDiscardedErrorCode is sent when tx was discarded.
|
|
||||||
SignRequestDiscardedErrorCode
|
|
||||||
)
|
|
||||||
|
|
||||||
var txReturnCodes = map[error]int{
|
|
||||||
nil: SignRequestNoErrorCode,
|
|
||||||
keystore.ErrDecrypt: SignRequestPasswordErrorCode,
|
|
||||||
ErrSignReqTimedOut: SignRequestTimeoutErrorCode,
|
|
||||||
ErrSignReqDiscarded: SignRequestDiscardedErrorCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendTransactionErrorCode(err error) int {
|
|
||||||
if code, ok := txReturnCodes[err]; ok {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
return SignRequestDefaultErrorCode
|
|
||||||
}
|
|
|
@ -1,181 +0,0 @@
|
||||||
package sign
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/status-im/status-go/account"
|
|
||||||
)
|
|
||||||
|
|
||||||
type verifyFunc func(string) (*account.SelectedExtKey, error)
|
|
||||||
|
|
||||||
// PendingRequests is a capped container that holds pending signing requests.
|
|
||||||
type PendingRequests struct {
|
|
||||||
mu sync.RWMutex // to guard transactions map
|
|
||||||
requests map[string]*Request
|
|
||||||
|
|
||||||
log log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPendingRequests creates a new requests list
|
|
||||||
func NewPendingRequests() *PendingRequests {
|
|
||||||
logger := log.New("package", "status-go/sign.PendingRequests")
|
|
||||||
|
|
||||||
return &PendingRequests{
|
|
||||||
requests: make(map[string]*Request),
|
|
||||||
log: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new signing request.
|
|
||||||
func (rs *PendingRequests) Add(ctx context.Context, method string, meta Meta, completeFunc CompleteFunc) (*Request, error) {
|
|
||||||
rs.mu.Lock()
|
|
||||||
defer rs.mu.Unlock()
|
|
||||||
|
|
||||||
request := newRequest(ctx, method, meta, completeFunc)
|
|
||||||
rs.requests[request.ID] = request
|
|
||||||
rs.log.Info("signing request is created", "ID", request.ID)
|
|
||||||
|
|
||||||
go SendSignRequestAdded(request)
|
|
||||||
|
|
||||||
return request, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a signing request by it's ID.
|
|
||||||
func (rs *PendingRequests) Get(id string) (*Request, error) {
|
|
||||||
rs.mu.RLock()
|
|
||||||
defer rs.mu.RUnlock()
|
|
||||||
|
|
||||||
if request, ok := rs.requests[id]; ok {
|
|
||||||
return request, nil
|
|
||||||
}
|
|
||||||
return nil, ErrSignReqNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// First returns a first signing request (if exists, nil otherwise).
|
|
||||||
func (rs *PendingRequests) First() *Request {
|
|
||||||
rs.mu.RLock()
|
|
||||||
defer rs.mu.RUnlock()
|
|
||||||
|
|
||||||
for _, req := range rs.requests {
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Approve a signing request by it's ID. Requires a valid password and a verification function.
|
|
||||||
func (rs *PendingRequests) Approve(id string, password string, args *TxArgs, verify verifyFunc) Result {
|
|
||||||
rs.log.Info("complete sign request", "id", id)
|
|
||||||
request, err := rs.tryLock(id)
|
|
||||||
if err != nil {
|
|
||||||
rs.log.Warn("can't process transaction", "err", err)
|
|
||||||
return newErrResult(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedAccount, err := verify(password)
|
|
||||||
if err != nil {
|
|
||||||
rs.complete(request, EmptyResponse, err)
|
|
||||||
return newErrResult(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := request.completeFunc(selectedAccount, password, args)
|
|
||||||
rs.log.Info("completed sign request ", "id", request.ID, "response", response, "err", err)
|
|
||||||
|
|
||||||
rs.complete(request, response, err)
|
|
||||||
|
|
||||||
return Result{
|
|
||||||
Response: response,
|
|
||||||
Error: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard remove a signing request from the list of pending requests.
|
|
||||||
func (rs *PendingRequests) Discard(id string) error {
|
|
||||||
request, err := rs.Get(id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rs.complete(request, EmptyResponse, ErrSignReqDiscarded)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait blocks until a request with a specified ID is completed (approved or discarded)
|
|
||||||
func (rs *PendingRequests) Wait(id string, timeout time.Duration) Result {
|
|
||||||
request, err := rs.Get(id)
|
|
||||||
if err != nil {
|
|
||||||
return newErrResult(err)
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case rst := <-request.result:
|
|
||||||
return rst
|
|
||||||
case <-time.After(timeout):
|
|
||||||
_, err := rs.tryLock(request.ID)
|
|
||||||
// if request is not already in progress, we complete it.
|
|
||||||
if err == nil {
|
|
||||||
rs.complete(request, EmptyResponse, ErrSignReqTimedOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count returns number of currently pending requests
|
|
||||||
func (rs *PendingRequests) Count() int {
|
|
||||||
rs.mu.RLock()
|
|
||||||
defer rs.mu.RUnlock()
|
|
||||||
return len(rs.requests)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has checks whether a pending request with a given identifier exists in the list
|
|
||||||
func (rs *PendingRequests) Has(id string) bool {
|
|
||||||
rs.mu.RLock()
|
|
||||||
defer rs.mu.RUnlock()
|
|
||||||
_, ok := rs.requests[id]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryLock is used to avoid double-completion of the same request.
|
|
||||||
// it returns a request instance if it isn't processing yet, returns an error otherwise.
|
|
||||||
func (rs *PendingRequests) tryLock(id string) (*Request, error) {
|
|
||||||
rs.mu.Lock()
|
|
||||||
defer rs.mu.Unlock()
|
|
||||||
if tx, ok := rs.requests[id]; ok {
|
|
||||||
if tx.locked {
|
|
||||||
return nil, ErrSignReqInProgress
|
|
||||||
}
|
|
||||||
tx.locked = true
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
return nil, ErrSignReqNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// complete removes the request from the list if there is no error or an error is non-transient
|
|
||||||
func (rs *PendingRequests) complete(request *Request, response Response, err error) {
|
|
||||||
rs.mu.Lock()
|
|
||||||
defer rs.mu.Unlock()
|
|
||||||
|
|
||||||
request.locked = false
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// TODO(divan): do we need the goroutine here?
|
|
||||||
go SendSignRequestFailed(request, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && isTransient(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(rs.requests, request.ID)
|
|
||||||
|
|
||||||
// response is updated only if err is nil, but transaction is not removed from a queue
|
|
||||||
result := Result{Error: err}
|
|
||||||
if err == nil {
|
|
||||||
result.Response = response
|
|
||||||
}
|
|
||||||
|
|
||||||
request.result <- result
|
|
||||||
}
|
|
|
@ -1,258 +0,0 @@
|
||||||
package sign
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"math/big"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
|
|
||||||
"github.com/status-im/status-go/account"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
correctPassword = "password-correct"
|
|
||||||
wrongPassword = "password-wrong"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
overridenGas = hexutil.Uint64(90002)
|
|
||||||
overridenGasPrice = (*hexutil.Big)(big.NewInt(20))
|
|
||||||
)
|
|
||||||
|
|
||||||
func testVerifyFunc(password string) (*account.SelectedExtKey, error) {
|
|
||||||
if password == correctPassword {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, keystore.ErrDecrypt
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPendingRequestsSuite(t *testing.T) {
|
|
||||||
suite.Run(t, new(PendingRequestsSuite))
|
|
||||||
}
|
|
||||||
|
|
||||||
type PendingRequestsSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
pendingRequests *PendingRequests
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) SetupTest() {
|
|
||||||
s.pendingRequests = NewPendingRequests()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) defaultSignTxArgs() *TxArgs {
|
|
||||||
return &TxArgs{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) defaultCompleteFunc() CompleteFunc {
|
|
||||||
hash := gethcommon.Hash{1}
|
|
||||||
return func(acc *account.SelectedExtKey, password string, args *TxArgs) (Response, error) {
|
|
||||||
s.Nil(acc, "account should be `nil`")
|
|
||||||
s.Equal(correctPassword, password)
|
|
||||||
return hash.Bytes(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) delayedCompleteFunc() CompleteFunc {
|
|
||||||
hash := gethcommon.Hash{1}
|
|
||||||
return func(acc *account.SelectedExtKey, password string, args *TxArgs) (Response, error) {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
s.Nil(acc, "account should be `nil`")
|
|
||||||
s.Equal(correctPassword, password)
|
|
||||||
return hash.Bytes(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) overridenCompleteFunc() CompleteFunc {
|
|
||||||
hash := gethcommon.Hash{1}
|
|
||||||
return func(acc *account.SelectedExtKey, password string, args *TxArgs) (Response, error) {
|
|
||||||
s.Nil(acc, "account should be `nil`")
|
|
||||||
s.Equal(correctPassword, password)
|
|
||||||
s.Equal(&overridenGas, args.Gas)
|
|
||||||
s.Equal(overridenGasPrice, args.GasPrice)
|
|
||||||
return hash.Bytes(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) errorCompleteFunc(err error) CompleteFunc {
|
|
||||||
hash := gethcommon.Hash{1}
|
|
||||||
return func(acc *account.SelectedExtKey, password string, args *TxArgs) (Response, error) {
|
|
||||||
s.Nil(acc, "account should be `nil`")
|
|
||||||
return hash.Bytes(), err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) TestGet() {
|
|
||||||
req, err := s.pendingRequests.Add(context.Background(), "", nil, s.defaultCompleteFunc())
|
|
||||||
s.NoError(err)
|
|
||||||
for i := 2; i > 0; i-- {
|
|
||||||
actualRequest, err := s.pendingRequests.Get(req.ID)
|
|
||||||
s.NoError(err)
|
|
||||||
s.Equal(req, actualRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) testComplete(password string, hash gethcommon.Hash, completeFunc CompleteFunc, signArgs *TxArgs) (string, error) {
|
|
||||||
req, err := s.pendingRequests.Add(context.Background(), "", nil, completeFunc)
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.True(s.pendingRequests.Has(req.ID), "sign request should exist")
|
|
||||||
|
|
||||||
result := s.pendingRequests.Approve(req.ID, password, signArgs, testVerifyFunc)
|
|
||||||
|
|
||||||
if s.pendingRequests.Has(req.ID) {
|
|
||||||
// transient error
|
|
||||||
s.Equal(EmptyResponse, result.Response, "no hash should be sent")
|
|
||||||
} else {
|
|
||||||
s.Equal(hash.Bytes(), result.Response.Bytes(), "hashes should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
return req.ID, result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) TestCompleteSuccess() {
|
|
||||||
id, err := s.testComplete(correctPassword, gethcommon.Hash{1}, s.defaultCompleteFunc(), s.defaultSignTxArgs())
|
|
||||||
s.NoError(err, "no errors should be there")
|
|
||||||
|
|
||||||
s.False(s.pendingRequests.Has(id), "sign request should not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) TestCompleteTransientError() {
|
|
||||||
hash := gethcommon.Hash{}
|
|
||||||
id, err := s.testComplete(wrongPassword, hash, s.errorCompleteFunc(keystore.ErrDecrypt), s.defaultSignTxArgs())
|
|
||||||
s.Equal(keystore.ErrDecrypt, err, "error value should be preserved")
|
|
||||||
|
|
||||||
s.True(s.pendingRequests.Has(id))
|
|
||||||
// verify that you are able to re-approve it after a transient error
|
|
||||||
_, err = s.pendingRequests.tryLock(id)
|
|
||||||
s.NoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) TestCompleteError() {
|
|
||||||
hash := gethcommon.Hash{1}
|
|
||||||
expectedError := errors.New("test")
|
|
||||||
|
|
||||||
id, err := s.testComplete(correctPassword, hash, s.errorCompleteFunc(expectedError), s.defaultSignTxArgs())
|
|
||||||
|
|
||||||
s.Equal(expectedError, err, "error value should be preserved")
|
|
||||||
|
|
||||||
s.False(s.pendingRequests.Has(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s PendingRequestsSuite) TestMultipleComplete() {
|
|
||||||
id, err := s.testComplete(correctPassword, gethcommon.Hash{1}, s.defaultCompleteFunc(), s.defaultSignTxArgs())
|
|
||||||
s.NoError(err, "no errors should be there")
|
|
||||||
|
|
||||||
result := s.pendingRequests.Approve(id, correctPassword, s.defaultSignTxArgs(), testVerifyFunc)
|
|
||||||
|
|
||||||
s.Equal(ErrSignReqNotFound, result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s PendingRequestsSuite) TestConcurrentComplete() {
|
|
||||||
req, err := s.pendingRequests.Add(context.Background(), "", nil, s.delayedCompleteFunc())
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.True(s.pendingRequests.Has(req.ID), "sign request should exist")
|
|
||||||
|
|
||||||
var approved int32
|
|
||||||
var tried int32
|
|
||||||
|
|
||||||
for i := 10; i > 0; i-- {
|
|
||||||
go func() {
|
|
||||||
result := s.pendingRequests.Approve(req.ID, correctPassword, s.defaultSignTxArgs(), testVerifyFunc)
|
|
||||||
if result.Error == nil {
|
|
||||||
atomic.AddInt32(&approved, 1)
|
|
||||||
}
|
|
||||||
atomic.AddInt32(&tried, 1)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
rst := s.pendingRequests.Wait(req.ID, 10*time.Second)
|
|
||||||
s.Require().NoError(rst.Error)
|
|
||||||
|
|
||||||
s.False(s.pendingRequests.Has(req.ID), "sign request should exist")
|
|
||||||
|
|
||||||
s.EqualValues(atomic.LoadInt32(&approved), 1, "request should be approved only once")
|
|
||||||
s.EqualValues(atomic.LoadInt32(&tried), 10, "request should be tried to approve 10 times")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s PendingRequestsSuite) TestWaitSuccess() {
|
|
||||||
req, err := s.pendingRequests.Add(context.Background(), "", nil, s.defaultCompleteFunc())
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.True(s.pendingRequests.Has(req.ID), "sign request should exist")
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
result := s.pendingRequests.Approve(req.ID, correctPassword, s.defaultSignTxArgs(), testVerifyFunc)
|
|
||||||
s.NoError(result.Error)
|
|
||||||
}()
|
|
||||||
|
|
||||||
result := s.pendingRequests.Wait(req.ID, 1*time.Second)
|
|
||||||
s.NoError(result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s PendingRequestsSuite) TestDiscard() {
|
|
||||||
req, err := s.pendingRequests.Add(context.Background(), "", nil, s.defaultCompleteFunc())
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.True(s.pendingRequests.Has(req.ID), "sign request should exist")
|
|
||||||
|
|
||||||
s.Equal(ErrSignReqNotFound, s.pendingRequests.Discard(""))
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// enough to make it be called after Wait
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
s.NoError(s.pendingRequests.Discard(req.ID))
|
|
||||||
}()
|
|
||||||
|
|
||||||
result := s.pendingRequests.Wait(req.ID, 1*time.Second)
|
|
||||||
s.Equal(ErrSignReqDiscarded, result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s PendingRequestsSuite) TestWaitFail() {
|
|
||||||
expectedError := errors.New("test-wait-fail")
|
|
||||||
req, err := s.pendingRequests.Add(context.Background(), "", nil, s.errorCompleteFunc(expectedError))
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.True(s.pendingRequests.Has(req.ID), "sign request should exist")
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
result := s.pendingRequests.Approve(req.ID, correctPassword, s.defaultSignTxArgs(), testVerifyFunc)
|
|
||||||
s.Equal(expectedError, result.Error)
|
|
||||||
}()
|
|
||||||
|
|
||||||
result := s.pendingRequests.Wait(req.ID, 1*time.Second)
|
|
||||||
s.Equal(expectedError, result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s PendingRequestsSuite) TestWaitTimeout() {
|
|
||||||
req, err := s.pendingRequests.Add(context.Background(), "", nil, s.defaultCompleteFunc())
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.True(s.pendingRequests.Has(req.ID), "sign request should exist")
|
|
||||||
|
|
||||||
result := s.pendingRequests.Wait(req.ID, 0*time.Second)
|
|
||||||
s.Equal(ErrSignReqTimedOut, result.Error)
|
|
||||||
|
|
||||||
// Try approving the timed out request, it will fail
|
|
||||||
result = s.pendingRequests.Approve(req.ID, correctPassword, s.defaultSignTxArgs(), testVerifyFunc)
|
|
||||||
s.NotNil(result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PendingRequestsSuite) TestCompleteSuccessWithOverridenGas() {
|
|
||||||
txArgs := TxArgs{
|
|
||||||
Gas: &overridenGas,
|
|
||||||
GasPrice: overridenGasPrice,
|
|
||||||
}
|
|
||||||
id, err := s.testComplete(correctPassword, gethcommon.Hash{1}, s.overridenCompleteFunc(), &txArgs)
|
|
||||||
s.NoError(err, "no errors should be there")
|
|
||||||
|
|
||||||
s.False(s.pendingRequests.Has(id), "sign request should not exist")
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package sign
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/pborman/uuid"
|
|
||||||
"github.com/status-im/status-go/account"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CompleteFunc is a function that is called after the sign request is approved.
|
|
||||||
type CompleteFunc func(account *account.SelectedExtKey, password string, completeArgs *TxArgs) (Response, error)
|
|
||||||
|
|
||||||
// Meta represents any metadata that could be attached to a signing request.
|
|
||||||
// It will be JSON-serialized and used in notifications to the API consumer.
|
|
||||||
type Meta interface{}
|
|
||||||
|
|
||||||
// Request is a single signing request.
|
|
||||||
type Request struct {
|
|
||||||
ID string
|
|
||||||
Method string
|
|
||||||
Meta Meta
|
|
||||||
context context.Context
|
|
||||||
locked bool
|
|
||||||
completeFunc CompleteFunc
|
|
||||||
result chan Result
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxArgs represents the arguments to submit when signing a transaction
|
|
||||||
type TxArgs struct {
|
|
||||||
Gas *hexutil.Uint64 `json:"gas"`
|
|
||||||
GasPrice *hexutil.Big `json:"gasPrice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRequest(ctx context.Context, method string, meta Meta, completeFunc CompleteFunc) *Request {
|
|
||||||
return &Request{
|
|
||||||
ID: uuid.New(),
|
|
||||||
Method: method,
|
|
||||||
Meta: meta,
|
|
||||||
context: ctx,
|
|
||||||
locked: false,
|
|
||||||
completeFunc: completeFunc,
|
|
||||||
result: make(chan Result, 1),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package sign
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Response is a byte payload returned by the signed function
|
|
||||||
type Response []byte
|
|
||||||
|
|
||||||
// Hex returns a string representation of the response
|
|
||||||
func (r Response) Hex() string {
|
|
||||||
return hexutil.Encode(r[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns a byte representation of the response
|
|
||||||
func (r Response) Bytes() []byte {
|
|
||||||
return []byte(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash converts response to a hash.
|
|
||||||
func (r Response) Hash() common.Hash {
|
|
||||||
return common.BytesToHash(r.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmptyResponse is returned when an error occures
|
|
||||||
var EmptyResponse = Response([]byte{})
|
|
||||||
|
|
||||||
// Result is a result of a signing request, error or successful
|
|
||||||
type Result struct {
|
|
||||||
Response Response
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// newErrResult creates a result based on an empty response and an error
|
|
||||||
func newErrResult(err error) Result {
|
|
||||||
return Result{
|
|
||||||
Response: EmptyResponse,
|
|
||||||
Error: err,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,13 +5,20 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/status-im/status-go/api"
|
"github.com/status-im/status-go/api"
|
||||||
"github.com/status-im/status-go/params"
|
|
||||||
"github.com/status-im/status-go/signal"
|
|
||||||
"github.com/status-im/status-go/t/e2e"
|
"github.com/status-im/status-go/t/e2e"
|
||||||
|
|
||||||
. "github.com/status-im/status-go/t/utils"
|
. "github.com/status-im/status-go/t/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// see vendor/github.com/ethereum/go-ethereum/rpc/errors.go:L27
|
||||||
|
methodNotFoundErrorCode = -32601
|
||||||
|
)
|
||||||
|
|
||||||
|
type rpcError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
type BaseJSONRPCSuite struct {
|
type BaseJSONRPCSuite struct {
|
||||||
e2e.BackendTestSuite
|
e2e.BackendTestSuite
|
||||||
}
|
}
|
||||||
|
@ -81,30 +88,3 @@ func (s *BaseJSONRPCSuite) SetupTest(upstreamEnabled, statusServiceEnabled, debu
|
||||||
|
|
||||||
return s.Backend.StartNode(nodeConfig)
|
return s.Backend.StartNode(nodeConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaseJSONRPCSuite) notificationHandler(account string, pass string, expectedError error) func(string) {
|
|
||||||
return func(jsonEvent string) {
|
|
||||||
envelope := unmarshalEnvelope(jsonEvent)
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
id := event["id"].(string)
|
|
||||||
s.T().Logf("Sign request added (will be completed shortly): {id: %s}\n", id)
|
|
||||||
|
|
||||||
//check for the correct method name
|
|
||||||
method := event["method"].(string)
|
|
||||||
s.Equal(params.PersonalSignMethodName, method)
|
|
||||||
//check the event data
|
|
||||||
args := event["args"].(map[string]interface{})
|
|
||||||
s.Equal(signDataString, args["data"].(string))
|
|
||||||
s.Equal(account, args["account"].(string))
|
|
||||||
|
|
||||||
e := s.Backend.ApproveSignRequest(id, pass).Error
|
|
||||||
s.T().Logf("Sign request approved. {id: %s, acc: %s, err: %v}", id, account, e)
|
|
||||||
if expectedError == nil {
|
|
||||||
s.NoError(e, "cannot complete sign reauest[%v]: %v", id, e)
|
|
||||||
} else {
|
|
||||||
s.EqualError(e, expectedError.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,47 +1,22 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
||||||
acc "github.com/status-im/status-go/account"
|
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/services/personal"
|
|
||||||
"github.com/status-im/status-go/signal"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
. "github.com/status-im/status-go/t/utils"
|
. "github.com/status-im/status-go/t/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
signDataString = "0xBAADBEEF"
|
signDataString = "0xBAADBEEF"
|
||||||
accountNotExists = "0x00164ca341326a03b547c05B343b2E21eFAe2400"
|
|
||||||
|
|
||||||
// see vendor/github.com/ethereum/go-ethereum/rpc/errors.go:L27
|
|
||||||
methodNotFoundErrorCode = -32601
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type rpcError struct {
|
type PersonalSignSuite struct {
|
||||||
Code int `json:"code"`
|
upstream bool
|
||||||
}
|
BaseJSONRPCSuite
|
||||||
|
|
||||||
type testParams struct {
|
|
||||||
Title string
|
|
||||||
EnableUpstream bool
|
|
||||||
Account string
|
|
||||||
Password string
|
|
||||||
HandlerFactory func(string, string) func(string)
|
|
||||||
ExpectedError error
|
|
||||||
DontSelectAccount bool // to take advantage of the fact, that the default is `false`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPersonalSignSuite(t *testing.T) {
|
|
||||||
s := new(PersonalSignSuite)
|
|
||||||
s.upstream = false
|
|
||||||
suite.Run(t, s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPersonalSignSuiteUpstream(t *testing.T) {
|
func TestPersonalSignSuiteUpstream(t *testing.T) {
|
||||||
|
@ -50,18 +25,13 @@ func TestPersonalSignSuiteUpstream(t *testing.T) {
|
||||||
suite.Run(t, s)
|
suite.Run(t, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersonalSignSuite struct {
|
|
||||||
BaseJSONRPCSuite
|
|
||||||
upstream bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) TestRestrictedPersonalAPIs() {
|
func (s *PersonalSignSuite) TestRestrictedPersonalAPIs() {
|
||||||
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
|
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
|
||||||
s.T().Skip()
|
s.T().Skip()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.SetupTest(s.upstream, false, false)
|
err := s.SetupTest(true, false, false)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
defer func() {
|
defer func() {
|
||||||
err := s.Backend.StopNode()
|
err := s.Backend.StopNode()
|
||||||
|
@ -79,172 +49,30 @@ func (s *PersonalSignSuite) TestRestrictedPersonalAPIs() {
|
||||||
s.AssertAPIMethodUnexported("personal_importRawKey")
|
s.AssertAPIMethodUnexported("personal_importRawKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PersonalSignSuite) TestPersonalSignSuccess() {
|
func (s *PersonalSignSuite) TestPersonalSignUnsupportedMethod() {
|
||||||
s.testPersonalSign(testParams{
|
|
||||||
Account: TestConfig.Account1.Address,
|
|
||||||
Password: TestConfig.Account1.Password,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) TestPersonalSignWrongPassword() {
|
|
||||||
s.testPersonalSign(testParams{
|
|
||||||
Account: TestConfig.Account1.Address,
|
|
||||||
Password: TestConfig.Account1.Password,
|
|
||||||
HandlerFactory: s.notificationHandlerWrongPassword,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) TestPersonalSignNoSuchAccount() {
|
|
||||||
s.testPersonalSign(testParams{
|
|
||||||
Account: accountNotExists,
|
|
||||||
Password: TestConfig.Account1.Password,
|
|
||||||
ExpectedError: personal.ErrInvalidPersonalSignAccount,
|
|
||||||
HandlerFactory: s.notificationHandlerNoAccount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) TestPersonalSignWrongAccount() {
|
|
||||||
s.testPersonalSign(testParams{
|
|
||||||
Account: TestConfig.Account2.Address,
|
|
||||||
Password: TestConfig.Account2.Password,
|
|
||||||
ExpectedError: personal.ErrInvalidPersonalSignAccount,
|
|
||||||
HandlerFactory: s.notificationHandlerInvalidAccount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) TestPersonalSignNoAccountSelected() {
|
|
||||||
s.testPersonalSign(testParams{
|
|
||||||
Account: TestConfig.Account1.Address,
|
|
||||||
Password: TestConfig.Account1.Password,
|
|
||||||
HandlerFactory: s.notificationHandlerNoAccountSelected,
|
|
||||||
DontSelectAccount: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
func (s *PersonalSignSuite) notificationHandlerWrongPassword(account string, pass string) func(string) {
|
|
||||||
return func(jsonEvent string) {
|
|
||||||
s.notificationHandler(account, pass+"wrong", keystore.ErrDecrypt)(jsonEvent)
|
|
||||||
s.notificationHandlerSuccess(account, pass)(jsonEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) notificationHandlerNoAccount(account string, pass string) func(string) {
|
|
||||||
return func(jsonEvent string) {
|
|
||||||
s.notificationHandler(account, pass, personal.ErrInvalidPersonalSignAccount)(jsonEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) notificationHandlerInvalidAccount(account string, pass string) func(string) {
|
|
||||||
return func(jsonEvent string) {
|
|
||||||
s.notificationHandler(account, pass, personal.ErrInvalidPersonalSignAccount)(jsonEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) notificationHandlerNoAccountSelected(account string, pass string) func(string) {
|
|
||||||
return func(jsonEvent string) {
|
|
||||||
s.notificationHandler(account, pass, acc.ErrNoAccountSelected)(jsonEvent)
|
|
||||||
envelope := unmarshalEnvelope(jsonEvent)
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
|
||||||
s.NoError(err)
|
|
||||||
}
|
|
||||||
s.notificationHandlerSuccess(account, pass)(jsonEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) notificationHandler(account string, pass string, expectedError error) func(string) {
|
|
||||||
return func(jsonEvent string) {
|
|
||||||
envelope := unmarshalEnvelope(jsonEvent)
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
id := event["id"].(string)
|
|
||||||
s.T().Logf("Sign request added (will be completed shortly): {id: %s}\n", id)
|
|
||||||
|
|
||||||
//check for the correct method name
|
|
||||||
method := event["method"].(string)
|
|
||||||
s.Equal(params.PersonalSignMethodName, method)
|
|
||||||
//check the event data
|
|
||||||
args := event["args"].(map[string]interface{})
|
|
||||||
s.Equal(signDataString, args["data"].(string))
|
|
||||||
s.Equal(account, args["account"].(string))
|
|
||||||
|
|
||||||
e := s.Backend.ApproveSignRequest(id, pass).Error
|
|
||||||
s.T().Logf("Sign request approved. {id: %s, acc: %s, err: %v}", id, account, e)
|
|
||||||
if expectedError == nil {
|
|
||||||
s.NoError(e, "cannot complete sign reauest[%v]: %v", id, e)
|
|
||||||
} else {
|
|
||||||
s.EqualError(e, expectedError.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) testPersonalSign(testParams testParams) string {
|
|
||||||
// Test upstream if that's not StatusChain
|
// Test upstream if that's not StatusChain
|
||||||
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
|
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
|
||||||
s.T().Skip()
|
s.T().Skip()
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if testParams.HandlerFactory == nil {
|
err := s.SetupTest(true, false, false)
|
||||||
testParams.HandlerFactory = s.notificationHandlerSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.SetupTest(s.upstream, false, false)
|
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
defer func() {
|
defer func() {
|
||||||
err := s.Backend.StopNode()
|
err := s.Backend.StopNode()
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
signal.SetDefaultNodeNotificationHandler(testParams.HandlerFactory(testParams.Account, testParams.Password))
|
|
||||||
|
|
||||||
if testParams.DontSelectAccount {
|
|
||||||
s.NoError(s.Backend.Logout())
|
|
||||||
} else {
|
|
||||||
s.NoError(s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
|
|
||||||
}
|
|
||||||
|
|
||||||
basicCall := fmt.Sprintf(
|
basicCall := fmt.Sprintf(
|
||||||
`{"jsonrpc":"2.0","method":"personal_sign","params":["%s", "%s"],"id":67}`,
|
`{"jsonrpc":"2.0","method":"personal_sign","params":["%s", "%s"],"id":67}`,
|
||||||
signDataString,
|
signDataString,
|
||||||
testParams.Account)
|
TestConfig.Account1.Address)
|
||||||
|
|
||||||
result := s.Backend.CallRPC(basicCall)
|
rawResult := s.Backend.CallRPC(basicCall)
|
||||||
if testParams.ExpectedError == nil {
|
|
||||||
s.NotContains(result, "error")
|
|
||||||
return s.extractResultFromRPCResponse(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Contains(result, testParams.ExpectedError.Error())
|
s.Contains(rawResult, `"error":{"code":-32700,"message":"method is unsupported by RPC interface"}`)
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PersonalSignSuite) extractResultFromRPCResponse(response string) string {
|
func (s *PersonalSignSuite) TestPersonalRecoverUnsupportedMethod() {
|
||||||
var r struct {
|
|
||||||
Result string `json:"result"`
|
|
||||||
}
|
|
||||||
s.NoError(json.Unmarshal([]byte(response), &r))
|
|
||||||
|
|
||||||
return r.Result
|
|
||||||
}
|
|
||||||
|
|
||||||
func unmarshalEnvelope(jsonEvent string) signal.Envelope {
|
|
||||||
var envelope signal.Envelope
|
|
||||||
if e := json.Unmarshal([]byte(jsonEvent), &envelope); e != nil {
|
|
||||||
panic(e)
|
|
||||||
}
|
|
||||||
return envelope
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PersonalSignSuite) TestPersonalRecoverSuccess() {
|
|
||||||
|
|
||||||
// 1. Sign
|
|
||||||
signedData := s.testPersonalSign(testParams{
|
|
||||||
Account: TestConfig.Account1.Address,
|
|
||||||
Password: TestConfig.Account1.Password,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test upstream if that's not StatusChain
|
// Test upstream if that's not StatusChain
|
||||||
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
|
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
|
||||||
|
@ -252,7 +80,7 @@ func (s *PersonalSignSuite) TestPersonalRecoverSuccess() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.SetupTest(s.upstream, false, false)
|
err := s.SetupTest(true, false, false)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
defer func() {
|
defer func() {
|
||||||
err := s.Backend.StopNode()
|
err := s.Backend.StopNode()
|
||||||
|
@ -263,17 +91,9 @@ func (s *PersonalSignSuite) TestPersonalRecoverSuccess() {
|
||||||
basicCall := fmt.Sprintf(
|
basicCall := fmt.Sprintf(
|
||||||
`{"jsonrpc":"2.0","method":"personal_ecRecover","params":["%s", "%s"],"id":67}`,
|
`{"jsonrpc":"2.0","method":"personal_ecRecover","params":["%s", "%s"],"id":67}`,
|
||||||
signDataString,
|
signDataString,
|
||||||
signedData)
|
"")
|
||||||
|
|
||||||
response := s.Backend.CallRPC(basicCall)
|
rawResult := s.Backend.CallRPC(basicCall)
|
||||||
|
|
||||||
result := s.extractResultFromRPCResponse(response)
|
s.Contains(rawResult, `"error":{"code":-32700,"message":"method is unsupported by RPC interface"}`)
|
||||||
|
|
||||||
s.True(strings.EqualFold(result, TestConfig.Account1.Address))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaseJSONRPCSuite) notificationHandlerSuccess(account string, pass string) func(string) {
|
|
||||||
return func(jsonEvent string) {
|
|
||||||
s.notificationHandler(account, pass, nil)(jsonEvent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/status-im/status-go/account"
|
"github.com/status-im/status-go/account"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/services/status"
|
"github.com/status-im/status-go/services/status"
|
||||||
"github.com/status-im/status-go/signal"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
. "github.com/status-im/status-go/t/utils"
|
. "github.com/status-im/status-go/t/utils"
|
||||||
|
@ -109,10 +108,6 @@ func (s *StatusAPISuite) testStatusLogin(testParams statusTestParams) *status.Lo
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if testParams.HandlerFactory == nil {
|
|
||||||
testParams.HandlerFactory = s.notificationHandlerSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.SetupTest(s.upstream, true, false)
|
err := s.SetupTest(s.upstream, true, false)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -120,8 +115,6 @@ func (s *StatusAPISuite) testStatusLogin(testParams statusTestParams) *status.Lo
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
signal.SetDefaultNodeNotificationHandler(testParams.HandlerFactory(testParams.Address, testParams.Password))
|
|
||||||
|
|
||||||
req := status.LoginRequest{
|
req := status.LoginRequest{
|
||||||
Addr: testParams.Address,
|
Addr: testParams.Address,
|
||||||
Password: testParams.Password,
|
Password: testParams.Password,
|
||||||
|
@ -155,10 +148,6 @@ func (s *StatusAPISuite) testStatusSignup(testParams statusTestParams) *status.S
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if testParams.HandlerFactory == nil {
|
|
||||||
testParams.HandlerFactory = s.notificationHandlerSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.SetupTest(s.upstream, true, false)
|
err := s.SetupTest(s.upstream, true, false)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -166,8 +155,6 @@ func (s *StatusAPISuite) testStatusSignup(testParams statusTestParams) *status.S
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
signal.SetDefaultNodeNotificationHandler(testParams.HandlerFactory(testParams.Address, testParams.Password))
|
|
||||||
|
|
||||||
req := status.SignupRequest{
|
req := status.SignupRequest{
|
||||||
Password: testParams.Password,
|
Password: testParams.Password,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
|
|
||||||
"github.com/status-im/status-go/api"
|
"github.com/status-im/status-go/api"
|
||||||
"github.com/status-im/status-go/node"
|
"github.com/status-im/status-go/node"
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
"github.com/status-im/status-go/signal"
|
"github.com/status-im/status-go/signal"
|
||||||
. "github.com/status-im/status-go/t/utils" //nolint: golint
|
. "github.com/status-im/status-go/t/utils" //nolint: golint
|
||||||
"github.com/status-im/status-go/transactions"
|
"github.com/status-im/status-go/transactions"
|
||||||
|
@ -129,11 +128,6 @@ func (s *BackendTestSuite) Transactor() *transactions.Transactor {
|
||||||
return s.Backend.Transactor()
|
return s.Backend.Transactor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// PendingSignRequests returns a reference to PendingSignRequests.
|
|
||||||
func (s *BackendTestSuite) PendingSignRequests() *sign.PendingRequests {
|
|
||||||
return s.Backend.PendingSignRequests()
|
|
||||||
}
|
|
||||||
|
|
||||||
func importTestAccounts(keyStoreDir string) (err error) {
|
func importTestAccounts(keyStoreDir string) (err error) {
|
||||||
logger.Debug("Import accounts to", "dir", keyStoreDir)
|
logger.Debug("Import accounts to", "dir", keyStoreDir)
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,22 @@
|
||||||
package transactions
|
package transactions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"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/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/status-im/status-go/account"
|
"github.com/status-im/status-go/account"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
"github.com/status-im/status-go/signal"
|
|
||||||
e2e "github.com/status-im/status-go/t/e2e"
|
e2e "github.com/status-im/status-go/t/e2e"
|
||||||
. "github.com/status-im/status-go/t/utils"
|
. "github.com/status-im/status-go/t/utils"
|
||||||
"github.com/status-im/status-go/transactions"
|
"github.com/status-im/status-go/transactions"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const invalidTxID = "invalid-tx-id"
|
|
||||||
|
|
||||||
type initFunc func([]byte, *transactions.SendTxArgs)
|
type initFunc func([]byte, *transactions.SendTxArgs)
|
||||||
|
|
||||||
func txURLString(result sign.Result) string {
|
|
||||||
return fmt.Sprintf("https://ropsten.etherscan.io/tx/%s", result.Response.Hash().Hex())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTransactionsTestSuite(t *testing.T) {
|
func TestTransactionsTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(TransactionsTestSuite))
|
suite.Run(t, new(TransactionsTestSuite))
|
||||||
}
|
}
|
||||||
|
@ -86,53 +71,17 @@ func (s *TransactionsTestSuite) sendTransactionUsingRPCClient(callRPCFn func(str
|
||||||
err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
transactionCompleted := make(chan struct{})
|
|
||||||
|
|
||||||
var signResult sign.Result
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(rawSignal string) {
|
|
||||||
var sg signal.Envelope
|
|
||||||
err := json.Unmarshal([]byte(rawSignal), &sg)
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
if sg.Type == signal.EventSignRequestAdded {
|
|
||||||
event := sg.Event.(map[string]interface{})
|
|
||||||
// check for the correct method name
|
|
||||||
method := event["method"].(string)
|
|
||||||
s.Equal(params.SendTransactionMethodName, method)
|
|
||||||
|
|
||||||
txID := event["id"].(string)
|
|
||||||
|
|
||||||
// Complete with a wrong passphrase.
|
|
||||||
signResult = s.Backend.ApproveSignRequest(txID, "some-invalid-passphrase")
|
|
||||||
s.EqualError(signResult.Error, keystore.ErrDecrypt.Error(), "should return an error as the passphrase was invalid")
|
|
||||||
|
|
||||||
// Complete with a correct passphrase.
|
|
||||||
signResult = s.Backend.ApproveSignRequest(txID, TestConfig.Account2.Password)
|
|
||||||
s.NoError(signResult.Error, "cannot complete queued transaction %s", txID)
|
|
||||||
|
|
||||||
close(transactionCompleted)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
result := callRPCFn(`{
|
result := callRPCFn(`{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"method": "eth_sendTransaction",
|
"method": "eth_sendTransaction",
|
||||||
"params": [{
|
"params": [{
|
||||||
"from": "` + TestConfig.Account1.Address + `",
|
"from": "` + TestConfig.Account1.Address + `",
|
||||||
"to": "` + TestConfig.Account2.Address + `",
|
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
|
||||||
"value": "0x9184e72a"
|
"value": "0x9184e72a"
|
||||||
}]
|
}]
|
||||||
}`)
|
}`)
|
||||||
s.NotContains(result, "error")
|
s.Contains(result, `"error":{"code":-32700,"message":"method is unsupported by RPC interface"}`)
|
||||||
|
|
||||||
select {
|
|
||||||
case <-transactionCompleted:
|
|
||||||
case <-time.After(time.Minute):
|
|
||||||
s.FailNow("sending transaction timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+signResult.Response.Hash().Hex()+`"}`, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestEmptyToFieldPreserved() {
|
func (s *TransactionsTestSuite) TestEmptyToFieldPreserved() {
|
||||||
|
@ -145,41 +94,13 @@ func (s *TransactionsTestSuite) TestEmptyToFieldPreserved() {
|
||||||
err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
transactionCompleted := make(chan struct{})
|
args := transactions.SendTxArgs{
|
||||||
signal.SetDefaultNodeNotificationHandler(func(rawSignal string) {
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
var sg struct {
|
|
||||||
Type string
|
|
||||||
Event json.RawMessage
|
|
||||||
}
|
|
||||||
err := json.Unmarshal([]byte(rawSignal), &sg)
|
|
||||||
s.NoError(err)
|
|
||||||
if sg.Type == signal.EventSignRequestAdded {
|
|
||||||
var event signal.PendingRequestEvent
|
|
||||||
s.NoError(json.Unmarshal(sg.Event, &event))
|
|
||||||
args := event.Args.(map[string]interface{})
|
|
||||||
s.NotNil(args["from"])
|
|
||||||
s.Nil(args["to"])
|
|
||||||
signResult := s.Backend.ApproveSignRequest(event.ID, TestConfig.Account1.Password)
|
|
||||||
s.NoError(signResult.Error)
|
|
||||||
close(transactionCompleted)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
result := s.Backend.CallRPC(`{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "eth_sendTransaction",
|
|
||||||
"params": [{
|
|
||||||
"from": "` + TestConfig.Account1.Address + `"
|
|
||||||
}]
|
|
||||||
}`)
|
|
||||||
s.NotContains(result, "error")
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-transactionCompleted:
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
s.FailNow("sending transaction timed out")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hash, err := s.Backend.SendTransaction(args, TestConfig.Account1.Password)
|
||||||
|
s.NoError(err)
|
||||||
|
s.NotNil(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSendContractCompat tries to send transaction using the legacy "Data"
|
// TestSendContractCompat tries to send transaction using the legacy "Data"
|
||||||
|
@ -232,79 +153,15 @@ func (s *TransactionsTestSuite) TestSendContractTx() {
|
||||||
s.testSendContractTx(initFunc, nil, "")
|
s.testSendContractTx(initFunc, nil, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) setDefaultNodeNotificationHandler(signRequestResult *[]byte, sampleAddress string, done chan struct{}, expectedError error) {
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint :dupl
|
|
||||||
var envelope signal.Envelope
|
|
||||||
err := json.Unmarshal([]byte(jsonEvent), &envelope)
|
|
||||||
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string))
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
err = s.Backend.ApproveSignRequest(
|
|
||||||
event["id"].(string),
|
|
||||||
TestConfig.Account1.Password,
|
|
||||||
).Error
|
|
||||||
s.EqualError(
|
|
||||||
err,
|
|
||||||
account.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.SelectAccount(sampleAddress, TestConfig.Account1.Password)
|
|
||||||
s.NoError(err)
|
|
||||||
err = s.Backend.ApproveSignRequest(
|
|
||||||
event["id"].(string),
|
|
||||||
TestConfig.Account1.Password,
|
|
||||||
).Error
|
|
||||||
s.EqualError(
|
|
||||||
err,
|
|
||||||
transactions.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 succeed")
|
|
||||||
s.NoError(s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
|
|
||||||
result := s.Backend.ApproveSignRequest(
|
|
||||||
event["id"].(string),
|
|
||||||
TestConfig.Account1.Password,
|
|
||||||
)
|
|
||||||
if expectedError != nil {
|
|
||||||
s.Equal(expectedError, result.Error)
|
|
||||||
} else {
|
|
||||||
s.NoError(result.Error, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"]))
|
|
||||||
}
|
|
||||||
|
|
||||||
*signRequestResult = result.Response.Bytes()[:]
|
|
||||||
|
|
||||||
log.Info("contract transaction complete", "URL", txURLString(result))
|
|
||||||
close(done)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) testSendContractTx(setInputAndDataValue initFunc, expectedError error, expectedErrorDescription string) {
|
func (s *TransactionsTestSuite) testSendContractTx(setInputAndDataValue initFunc, expectedError error, expectedErrorDescription string) {
|
||||||
s.StartTestBackend()
|
s.StartTestBackend()
|
||||||
defer s.StopTestBackend()
|
defer s.StopTestBackend()
|
||||||
|
|
||||||
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
||||||
|
|
||||||
sampleAddress, _, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
err := s.Backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
completeQueuedTransaction := make(chan struct{})
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var signRequestResult []byte
|
|
||||||
s.setDefaultNodeNotificationHandler(&signRequestResult, sampleAddress, completeQueuedTransaction, expectedError)
|
|
||||||
|
|
||||||
// this call blocks, up until Complete Transaction is called
|
// this call blocks, up until Complete Transaction is called
|
||||||
byteCode, err := hexutil.Decode(`0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029`)
|
byteCode, err := hexutil.Decode(`0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029`)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
@ -318,24 +175,13 @@ func (s *TransactionsTestSuite) testSendContractTx(setInputAndDataValue initFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputAndDataValue(byteCode, &args)
|
setInputAndDataValue(byteCode, &args)
|
||||||
txHashCheck, err := s.Backend.SendTransaction(context.TODO(), args)
|
hash, err := s.Backend.SendTransaction(args, TestConfig.Account1.Password)
|
||||||
|
|
||||||
if expectedError != nil {
|
if expectedError != nil {
|
||||||
s.Equal(expectedError, err, expectedErrorDescription)
|
s.Equal(expectedError, err, expectedErrorDescription)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.NoError(err, "cannot send transaction")
|
s.NoError(err)
|
||||||
|
s.False(reflect.DeepEqual(hash, gethcommon.Hash{}))
|
||||||
select {
|
|
||||||
case <-completeQueuedTransaction:
|
|
||||||
case <-time.After(2 * time.Minute):
|
|
||||||
s.FailNow("completing transaction timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Equal(txHashCheck.Bytes(), signRequestResult, "transaction hash returned from SendTransaction is invalid")
|
|
||||||
s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed")
|
|
||||||
s.Zero(s.PendingSignRequests().Count(), "tx queue must be empty at this point")
|
|
||||||
|
|
||||||
s.NoError(s.Backend.Logout())
|
s.NoError(s.Backend.Logout())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,33 +193,16 @@ func (s *TransactionsTestSuite) TestSendEther() {
|
||||||
|
|
||||||
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
||||||
|
|
||||||
// create an account
|
err := s.Backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
||||||
sampleAddress, _, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
completeQueuedTransaction := make(chan struct{})
|
hash, err := s.Backend.SendTransaction(transactions.SendTxArgs{
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var signRequestResult []byte
|
|
||||||
s.setDefaultNodeNotificationHandler(&signRequestResult, sampleAddress, completeQueuedTransaction, nil)
|
|
||||||
|
|
||||||
// this call blocks, up until Complete Transaction is called
|
|
||||||
txHashCheck, err := s.Backend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
||||||
})
|
}, TestConfig.Account1.Password)
|
||||||
s.NoError(err, "cannot send transaction")
|
s.NoError(err)
|
||||||
|
s.False(reflect.DeepEqual(hash, gethcommon.Hash{}))
|
||||||
select {
|
|
||||||
case <-completeQueuedTransaction:
|
|
||||||
case <-time.After(2 * time.Minute):
|
|
||||||
s.FailNow("completing transaction timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Equal(txHashCheck.Bytes(), signRequestResult, "transaction hash returned from SendTransaction is invalid")
|
|
||||||
s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed")
|
|
||||||
s.Zero(s.Backend.PendingSignRequests().Count(), "tx queue must be empty at this point")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestSendEtherTxUpstream() {
|
func (s *TransactionsTestSuite) TestSendEtherTxUpstream() {
|
||||||
|
@ -387,460 +216,12 @@ func (s *TransactionsTestSuite) TestSendEtherTxUpstream() {
|
||||||
err = s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
err = s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
completeQueuedTransaction := make(chan struct{})
|
hash, err := s.Backend.SendTransaction(transactions.SendTxArgs{
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var txHash = gethcommon.Hash{}
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint: dupl
|
|
||||||
var envelope signal.Envelope
|
|
||||||
err = json.Unmarshal([]byte(jsonEvent), &envelope)
|
|
||||||
s.NoError(err, "cannot unmarshal JSON: %s", jsonEvent)
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string))
|
|
||||||
|
|
||||||
signResult := s.Backend.ApproveSignRequest(
|
|
||||||
event["id"].(string),
|
|
||||||
TestConfig.Account1.Password,
|
|
||||||
)
|
|
||||||
s.NoError(signResult.Error, "cannot complete queued transaction[%v]", event["id"])
|
|
||||||
|
|
||||||
txHash = signResult.Response.Hash()
|
|
||||||
log.Info("contract transaction complete", "URL", txURLString(signResult))
|
|
||||||
close(completeQueuedTransaction)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// This call blocks, up until Complete Transaction is called.
|
|
||||||
// Explicitly not setting Gas to get it estimated.
|
|
||||||
txHashCheck, err := s.Backend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
GasPrice: (*hexutil.Big)(big.NewInt(28000000000)),
|
GasPrice: (*hexutil.Big)(big.NewInt(28000000000)),
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
||||||
})
|
}, TestConfig.Account1.Password)
|
||||||
s.NoError(err, "cannot send transaction")
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-completeQueuedTransaction:
|
|
||||||
case <-time.After(1 * time.Minute):
|
|
||||||
s.FailNow("completing transaction timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Equal(txHash.Hex(), txHashCheck.Hex(), "transaction hash returned from SendTransaction is invalid")
|
|
||||||
s.Zero(s.Backend.PendingSignRequests().Count(), "tx queue must be empty at this point")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestDoubleCompleteQueuedTransactions() {
|
|
||||||
CheckTestSkipForNetworks(s.T(), params.MainNetworkID)
|
|
||||||
|
|
||||||
s.StartTestBackend()
|
|
||||||
defer s.StopTestBackend()
|
|
||||||
|
|
||||||
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
|
||||||
s.NoError(s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
|
|
||||||
|
|
||||||
completeQueuedTransaction := make(chan struct{})
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var isTxFailedEventCalled int32 // using int32 as bool to avoid data race: 0 is `false`, 1 is `true`
|
|
||||||
signHash := gethcommon.Hash{}
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var envelope signal.Envelope
|
|
||||||
err := json.Unmarshal([]byte(jsonEvent), &envelope)
|
|
||||||
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
txID := event["id"].(string)
|
|
||||||
log.Info("transaction queued (will be failed and completed on the second call)", "id", txID)
|
|
||||||
|
|
||||||
// try with wrong password
|
|
||||||
// make sure that tx is NOT removed from the queue (by re-trying with the correct password)
|
|
||||||
err = s.Backend.ApproveSignRequest(txID, TestConfig.Account1.Password+"wrong").Error
|
|
||||||
s.EqualError(err, keystore.ErrDecrypt.Error())
|
|
||||||
|
|
||||||
s.Equal(1, s.PendingSignRequests().Count(), "txqueue cannot be empty, as tx has failed")
|
|
||||||
|
|
||||||
// now try to complete transaction, but with the correct password
|
|
||||||
signResult := s.Backend.ApproveSignRequest(txID, TestConfig.Account1.Password)
|
|
||||||
s.NoError(signResult.Error)
|
|
||||||
|
|
||||||
log.Info("transaction complete", "URL", txURLString(signResult))
|
|
||||||
|
|
||||||
signHash = signResult.Response.Hash()
|
|
||||||
|
|
||||||
close(completeQueuedTransaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestFailed {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
log.Info("transaction return event received", "id", event["id"].(string))
|
|
||||||
|
|
||||||
receivedErrMessage := event["error_message"].(string)
|
|
||||||
expectedErrMessage := "could not decrypt key with given passphrase"
|
|
||||||
s.Equal(expectedErrMessage, receivedErrMessage)
|
|
||||||
|
|
||||||
receivedErrCode := event["error_code"].(string)
|
|
||||||
s.Equal("2", receivedErrCode)
|
|
||||||
|
|
||||||
atomic.AddInt32(&isTxFailedEventCalled, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// this call blocks, and should return on *second* attempt to ApproveSignRequest (w/ the correct password)
|
|
||||||
sendTxHash, err := s.Backend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
|
||||||
})
|
|
||||||
s.NoError(err, "cannot send transaction")
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-completeQueuedTransaction:
|
|
||||||
case <-time.After(time.Minute):
|
|
||||||
s.FailNow("test timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Equal(sendTxHash, signHash, "transaction hash returned from SendTransaction is invalid")
|
|
||||||
s.False(reflect.DeepEqual(sendTxHash, gethcommon.Hash{}), "transaction was never queued or completed")
|
|
||||||
s.Zero(s.Backend.PendingSignRequests().Count(), "tx queue must be empty at this point")
|
|
||||||
s.True(atomic.LoadInt32(&isTxFailedEventCalled) > 0, "expected tx failure signal is not received")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestDiscardQueuedTransaction() {
|
|
||||||
CheckTestSkipForNetworks(s.T(), params.MainNetworkID)
|
|
||||||
|
|
||||||
s.StartTestBackend()
|
|
||||||
defer s.StopTestBackend()
|
|
||||||
|
|
||||||
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
|
||||||
s.NoError(s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
|
|
||||||
|
|
||||||
completeQueuedTransaction := make(chan struct{})
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var isTxFailedEventCalled int32 // using int32 as bool to avoid data race: 0 = `false`, 1 = `true`
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var envelope signal.Envelope
|
|
||||||
err := json.Unmarshal([]byte(jsonEvent), &envelope)
|
|
||||||
s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
txID := event["id"].(string)
|
|
||||||
log.Info("transaction queued (will be discarded soon)", "id", txID)
|
|
||||||
|
|
||||||
s.True(s.Backend.PendingSignRequests().Has(txID), "txqueue should still have test tx")
|
|
||||||
|
|
||||||
// discard
|
|
||||||
err := s.Backend.DiscardSignRequest(txID)
|
|
||||||
s.NoError(err, "cannot discard tx")
|
|
||||||
|
|
||||||
// try completing discarded transaction
|
|
||||||
err = s.Backend.ApproveSignRequest(txID, TestConfig.Account1.Password).Error
|
|
||||||
s.EqualError(err, sign.ErrSignReqNotFound.Error(), "expects tx not found, but call to ApproveSignRequest succeeded")
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
|
|
||||||
s.False(s.Backend.PendingSignRequests().Has(txID),
|
|
||||||
fmt.Sprintf("txqueue should not have test tx at this point (it should be discarded): %s", txID))
|
|
||||||
|
|
||||||
close(completeQueuedTransaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestFailed {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
log.Info("transaction return event received", "id", event["id"].(string))
|
|
||||||
|
|
||||||
receivedErrMessage := event["error_message"].(string)
|
|
||||||
expectedErrMessage := sign.ErrSignReqDiscarded.Error()
|
|
||||||
s.Equal(receivedErrMessage, expectedErrMessage)
|
|
||||||
|
|
||||||
receivedErrCode := event["error_code"].(string)
|
|
||||||
s.Equal("4", receivedErrCode)
|
|
||||||
|
|
||||||
atomic.AddInt32(&isTxFailedEventCalled, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// this call blocks, and should return when DiscardQueuedTransaction() is called
|
|
||||||
txHashCheck, err := s.Backend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
|
||||||
})
|
|
||||||
s.EqualError(err, sign.ErrSignReqDiscarded.Error(), "transaction is expected to be discarded")
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-completeQueuedTransaction:
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
s.FailNow("test timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.True(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction returned hash, while it shouldn't")
|
|
||||||
s.Zero(s.Backend.PendingSignRequests().Count(), "tx queue must be empty at this point")
|
|
||||||
s.True(atomic.LoadInt32(&isTxFailedEventCalled) > 0, "expected tx failure signal is not received")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestCompleteMultipleQueuedTransactions() {
|
|
||||||
CheckTestSkipForNetworks(s.T(), params.MainNetworkID)
|
|
||||||
|
|
||||||
s.setupLocalNode()
|
|
||||||
defer s.StopTestBackend()
|
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
|
||||||
err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
s.False(reflect.DeepEqual(hash, gethcommon.Hash{}))
|
||||||
s.sendConcurrentTransactions(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestDiscardMultipleQueuedTransactions() {
|
|
||||||
CheckTestSkipForNetworks(s.T(), params.MainNetworkID)
|
|
||||||
|
|
||||||
s.StartTestBackend()
|
|
||||||
defer s.StopTestBackend()
|
|
||||||
|
|
||||||
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
|
||||||
s.NoError(s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
|
|
||||||
|
|
||||||
testTxCount := 3
|
|
||||||
txIDs := make(chan string, testTxCount)
|
|
||||||
allTestTxDiscarded := make(chan struct{})
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
var txFailedEventCallCount int32
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var envelope signal.Envelope
|
|
||||||
err := json.Unmarshal([]byte(jsonEvent), &envelope)
|
|
||||||
s.NoError(err)
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
txID := event["id"].(string)
|
|
||||||
log.Info("transaction queued (will be discarded soon)", "id", txID)
|
|
||||||
|
|
||||||
s.True(s.Backend.PendingSignRequests().Has(txID),
|
|
||||||
"txqueue should still have test tx")
|
|
||||||
txIDs <- txID
|
|
||||||
}
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestFailed {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
log.Info("transaction return event received", "id", event["id"].(string))
|
|
||||||
|
|
||||||
receivedErrMessage := event["error_message"].(string)
|
|
||||||
expectedErrMessage := sign.ErrSignReqDiscarded.Error()
|
|
||||||
s.Equal(receivedErrMessage, expectedErrMessage)
|
|
||||||
|
|
||||||
receivedErrCode := event["error_code"].(string)
|
|
||||||
s.Equal("4", receivedErrCode)
|
|
||||||
|
|
||||||
newCount := atomic.AddInt32(&txFailedEventCallCount, 1)
|
|
||||||
if newCount == int32(testTxCount) {
|
|
||||||
close(allTestTxDiscarded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
require := s.Require()
|
|
||||||
|
|
||||||
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
|
|
||||||
sendTx := func() {
|
|
||||||
txHashCheck, err := s.Backend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
|
||||||
})
|
|
||||||
require.EqualError(err, sign.ErrSignReqDiscarded.Error())
|
|
||||||
require.Equal(gethcommon.Hash{}, txHashCheck, "transaction returned hash, while it shouldn't")
|
|
||||||
}
|
|
||||||
|
|
||||||
signRequests := s.Backend.PendingSignRequests()
|
|
||||||
|
|
||||||
// wait for transactions, and discard immediately
|
|
||||||
discardTxs := func(txIDs []string) {
|
|
||||||
txIDs = append(txIDs, invalidTxID)
|
|
||||||
|
|
||||||
// discard
|
|
||||||
discardResults := s.Backend.DiscardSignRequests(txIDs)
|
|
||||||
require.Len(discardResults, 1, "cannot discard txs: %v", discardResults)
|
|
||||||
require.Error(discardResults[invalidTxID], sign.ErrSignReqNotFound, "cannot discard txs: %v", discardResults)
|
|
||||||
|
|
||||||
// try completing discarded transaction
|
|
||||||
completeResults := s.Backend.ApproveSignRequests(txIDs, TestConfig.Account1.Password)
|
|
||||||
require.Len(completeResults, testTxCount+1, "unexpected number of errors (call to ApproveSignRequest should not succeed)")
|
|
||||||
|
|
||||||
for _, txResult := range completeResults {
|
|
||||||
require.Error(txResult.Error, sign.ErrSignReqNotFound, "invalid error for %s", txResult.Response.Hex())
|
|
||||||
require.Equal(sign.EmptyResponse, txResult.Response, "invalid hash (expected zero): %s", txResult.Response.Hex())
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
|
|
||||||
|
|
||||||
for _, txID := range txIDs {
|
|
||||||
require.False(
|
|
||||||
signRequests.Has(txID),
|
|
||||||
"txqueue should not have test tx at this point (it should be discarded): %s",
|
|
||||||
txID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
ids := make([]string, testTxCount)
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
ids[i] = <-txIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
discardTxs(ids)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// send multiple transactions
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
go sendTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-allTestTxDiscarded:
|
|
||||||
case <-time.After(1 * time.Minute):
|
|
||||||
s.FailNow("test timed out")
|
|
||||||
}
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
|
|
||||||
s.Zero(s.Backend.PendingSignRequests().Count(), "tx queue must be empty at this point")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestNonExistentQueuedTransactions() {
|
|
||||||
s.StartTestBackend()
|
|
||||||
defer s.StopTestBackend()
|
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
|
||||||
s.NoError(s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password))
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(string) {})
|
|
||||||
|
|
||||||
// try completing non-existing transaction
|
|
||||||
err := s.Backend.ApproveSignRequest("some-bad-transaction-id", TestConfig.Account1.Password).Error
|
|
||||||
s.Error(err, "error expected and not received")
|
|
||||||
s.EqualError(err, sign.ErrSignReqNotFound.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) TestCompleteMultipleQueuedTransactionsUpstream() {
|
|
||||||
CheckTestSkipForNetworks(s.T(), params.MainNetworkID)
|
|
||||||
|
|
||||||
s.setupUpstreamNode()
|
|
||||||
defer s.StopTestBackend()
|
|
||||||
|
|
||||||
// log into account from which transactions will be sent
|
|
||||||
err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.sendConcurrentTransactions(30)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) setupLocalNode() {
|
|
||||||
s.StartTestBackend()
|
|
||||||
|
|
||||||
EnsureNodeSync(s.Backend.StatusNode().EnsureSync)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) setupUpstreamNode() {
|
|
||||||
if GetNetworkID() == params.StatusChainNetworkID {
|
|
||||||
s.T().Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
addr, err := GetRemoteURL()
|
|
||||||
s.NoError(err)
|
|
||||||
s.StartTestBackend(e2e.WithUpstream(addr))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TransactionsTestSuite) sendConcurrentTransactions(testTxCount int) {
|
|
||||||
txIDs := make(chan string, testTxCount)
|
|
||||||
allTestTxCompleted := make(chan struct{})
|
|
||||||
|
|
||||||
require := s.Require()
|
|
||||||
|
|
||||||
// replace transaction notification handler
|
|
||||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
||||||
var envelope signal.Envelope
|
|
||||||
err := json.Unmarshal([]byte(jsonEvent), &envelope)
|
|
||||||
require.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent))
|
|
||||||
|
|
||||||
if envelope.Type == signal.EventSignRequestAdded {
|
|
||||||
event := envelope.Event.(map[string]interface{})
|
|
||||||
txID := event["id"].(string)
|
|
||||||
log.Info("transaction queued (will be completed in a single call, once aggregated)", "id", txID)
|
|
||||||
|
|
||||||
txIDs <- txID
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
|
|
||||||
sendTx := func() {
|
|
||||||
txHashCheck, err := s.Backend.SendTransaction(context.TODO(), transactions.SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
|
|
||||||
})
|
|
||||||
require.NoError(err, "cannot send transaction")
|
|
||||||
require.NotEqual(gethcommon.Hash{}, txHashCheck, "transaction returned empty hash")
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for transactions, and complete them in a single call
|
|
||||||
completeTxs := func(txIDs []string) {
|
|
||||||
txIDs = append(txIDs, invalidTxID)
|
|
||||||
results := s.Backend.ApproveSignRequests(txIDs, TestConfig.Account1.Password)
|
|
||||||
s.Len(results, testTxCount+1)
|
|
||||||
s.EqualError(results[invalidTxID].Error, sign.ErrSignReqNotFound.Error())
|
|
||||||
|
|
||||||
for txID, txResult := range results {
|
|
||||||
s.False(
|
|
||||||
txResult.Error != nil && txID != invalidTxID,
|
|
||||||
"invalid error for %s", txID,
|
|
||||||
)
|
|
||||||
s.False(
|
|
||||||
len(txResult.Response.Bytes()) < 1 && txID != invalidTxID,
|
|
||||||
"invalid hash (expected non empty hash): %s", txID,
|
|
||||||
)
|
|
||||||
log.Info("transaction complete", "URL", txURLString(txResult))
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
|
|
||||||
|
|
||||||
for _, txID := range txIDs {
|
|
||||||
s.False(
|
|
||||||
s.Backend.PendingSignRequests().Has(txID),
|
|
||||||
"txqueue should not have test tx at this point (it should be completed)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
ids := make([]string, testTxCount)
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
ids[i] = <-txIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
completeTxs(ids)
|
|
||||||
close(allTestTxCompleted)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// send multiple transactions
|
|
||||||
for i := 0; i < testTxCount; i++ {
|
|
||||||
go sendTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-allTestTxCompleted:
|
|
||||||
case <-time.After(60 * time.Second):
|
|
||||||
s.FailNow("test timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Zero(s.PendingSignRequests().Count(), "queue should be empty")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package transactions
|
package transactions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -14,9 +14,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
|
||||||
"github.com/status-im/status-go/account"
|
"github.com/status-im/status-go/account"
|
||||||
"github.com/status-im/status-go/params"
|
|
||||||
"github.com/status-im/status-go/rpc"
|
"github.com/status-im/status-go/rpc"
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -29,7 +27,6 @@ const (
|
||||||
// Transactor validates, signs transactions.
|
// Transactor validates, signs transactions.
|
||||||
// It uses upstream to propagate transactions to the Ethereum network.
|
// It uses upstream to propagate transactions to the Ethereum network.
|
||||||
type Transactor struct {
|
type Transactor struct {
|
||||||
pendingSignRequests *sign.PendingRequests
|
|
||||||
sender ethereum.TransactionSender
|
sender ethereum.TransactionSender
|
||||||
pendingNonceProvider PendingNonceProvider
|
pendingNonceProvider PendingNonceProvider
|
||||||
gasCalculator GasCalculator
|
gasCalculator GasCalculator
|
||||||
|
@ -43,13 +40,12 @@ type Transactor struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTransactor returns a new Manager.
|
// NewTransactor returns a new Manager.
|
||||||
func NewTransactor(signRequests *sign.PendingRequests) *Transactor {
|
func NewTransactor() *Transactor {
|
||||||
return &Transactor{
|
return &Transactor{
|
||||||
pendingSignRequests: signRequests,
|
addrLock: &AddrLocker{},
|
||||||
addrLock: &AddrLocker{},
|
sendTxTimeout: sendTxTimeout,
|
||||||
sendTxTimeout: sendTxTimeout,
|
localNonce: sync.Map{},
|
||||||
localNonce: sync.Map{},
|
log: log.New("package", "status-go/transactions.Manager"),
|
||||||
log: log.New("package", "status-go/transactions.Manager"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,25 +64,9 @@ func (t *Transactor) SetRPC(rpcClient *rpc.Client, timeout time.Duration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTransaction is an implementation of eth_sendTransaction. It queues the tx to the sign queue.
|
// SendTransaction is an implementation of eth_sendTransaction. It queues the tx to the sign queue.
|
||||||
func (t *Transactor) SendTransaction(ctx context.Context, args SendTxArgs) (gethcommon.Hash, error) {
|
func (t *Transactor) SendTransaction(sendArgs SendTxArgs, verifiedAccount *account.SelectedExtKey) (hash gethcommon.Hash, err error) {
|
||||||
if ctx == nil {
|
hash, err = t.validateAndPropagate(verifiedAccount, sendArgs)
|
||||||
ctx = context.Background()
|
return
|
||||||
}
|
|
||||||
|
|
||||||
completeFunc := func(acc *account.SelectedExtKey, password string, signArgs *sign.TxArgs) (sign.Response, error) {
|
|
||||||
t.mergeSignTxArgsOntoSendTxArgs(signArgs, &args)
|
|
||||||
hash, err := t.validateAndPropagate(acc, args)
|
|
||||||
return sign.Response(hash.Bytes()), err
|
|
||||||
}
|
|
||||||
|
|
||||||
request, err := t.pendingSignRequests.Add(ctx, params.SendTransactionMethodName, args, completeFunc)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return gethcommon.Hash{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := t.pendingSignRequests.Wait(request.ID, t.sendTxTimeout)
|
|
||||||
return result.Response.Hash(), result.Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure that only account which created the tx can complete it
|
// make sure that only account which created the tx can complete it
|
||||||
|
@ -95,10 +75,8 @@ func (t *Transactor) validateAccount(args SendTxArgs, selectedAccount *account.S
|
||||||
return account.ErrNoAccountSelected
|
return account.ErrNoAccountSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.From.Hex() != selectedAccount.Address.Hex() {
|
if !bytes.Equal(args.From.Bytes(), selectedAccount.Address.Bytes()) {
|
||||||
err := sign.NewTransientError(ErrInvalidCompleteTxSender)
|
return ErrInvalidTxSender
|
||||||
t.log.Error("queued transaction does not belong to the selected account", "err", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -108,6 +86,7 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe
|
||||||
if err = t.validateAccount(args, selectedAccount); err != nil {
|
if err = t.validateAccount(args, selectedAccount); err != nil {
|
||||||
return hash, err
|
return hash, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !args.Valid() {
|
if !args.Valid() {
|
||||||
return hash, ErrInvalidSendTxArgs
|
return hash, ErrInvalidSendTxArgs
|
||||||
}
|
}
|
||||||
|
@ -165,7 +144,7 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe
|
||||||
return hash, err
|
return hash, err
|
||||||
}
|
}
|
||||||
if gas < defaultGas {
|
if gas < defaultGas {
|
||||||
t.log.Info(fmt.Sprintf("default gas will be used. estimated gas %v is lower than %v", gas, defaultGas))
|
t.log.Info("default gas will be used because estimated is lower", "estimated", gas, "default", defaultGas)
|
||||||
gas = defaultGas
|
gas = defaultGas
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -205,15 +184,3 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe
|
||||||
}
|
}
|
||||||
return signedTx.Hash(), nil
|
return signedTx.Hash(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transactor) mergeSignTxArgsOntoSendTxArgs(signArgs *sign.TxArgs, args *SendTxArgs) {
|
|
||||||
if signArgs == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if signArgs.Gas != nil {
|
|
||||||
args.Gas = signArgs.Gas
|
|
||||||
}
|
|
||||||
if signArgs.GasPrice != nil {
|
|
||||||
args.GasPrice = signArgs.GasPrice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -24,23 +25,16 @@ import (
|
||||||
"github.com/status-im/status-go/account"
|
"github.com/status-im/status-go/account"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/rpc"
|
"github.com/status-im/status-go/rpc"
|
||||||
"github.com/status-im/status-go/sign"
|
|
||||||
"github.com/status-im/status-go/transactions/fake"
|
"github.com/status-im/status-go/transactions/fake"
|
||||||
|
|
||||||
. "github.com/status-im/status-go/t/utils"
|
. "github.com/status-im/status-go/t/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func simpleVerifyFunc(acc *account.SelectedExtKey) func(string) (*account.SelectedExtKey, error) {
|
func TestTransactorSuite(t *testing.T) {
|
||||||
return func(string) (*account.SelectedExtKey, error) {
|
suite.Run(t, new(TransactorSuite))
|
||||||
return acc, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTxQueueTestSuite(t *testing.T) {
|
type TransactorSuite struct {
|
||||||
suite.Run(t, new(TxQueueTestSuite))
|
|
||||||
}
|
|
||||||
|
|
||||||
type TxQueueTestSuite struct {
|
|
||||||
suite.Suite
|
suite.Suite
|
||||||
server *gethrpc.Server
|
server *gethrpc.Server
|
||||||
client *gethrpc.Client
|
client *gethrpc.Client
|
||||||
|
@ -51,7 +45,7 @@ type TxQueueTestSuite struct {
|
||||||
manager *Transactor
|
manager *Transactor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) SetupTest() {
|
func (s *TransactorSuite) SetupTest() {
|
||||||
s.txServiceMockCtrl = gomock.NewController(s.T())
|
s.txServiceMockCtrl = gomock.NewController(s.T())
|
||||||
|
|
||||||
s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
|
s.server, s.txServiceMock = fake.NewTestServer(s.txServiceMockCtrl)
|
||||||
|
@ -63,55 +57,49 @@ func (s *TxQueueTestSuite) SetupTest() {
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.nodeConfig = nodeConfig
|
s.nodeConfig = nodeConfig
|
||||||
|
|
||||||
s.manager = NewTransactor(sign.NewPendingRequests())
|
s.manager = NewTransactor()
|
||||||
s.manager.sendTxTimeout = time.Second
|
s.manager.sendTxTimeout = time.Second
|
||||||
s.manager.SetNetworkID(chainID)
|
s.manager.SetNetworkID(chainID)
|
||||||
s.manager.SetRPC(rpcClient, time.Second)
|
s.manager.SetRPC(rpcClient, time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TearDownTest() {
|
func (s *TransactorSuite) TearDownTest() {
|
||||||
s.txServiceMockCtrl.Finish()
|
s.txServiceMockCtrl.Finish()
|
||||||
s.server.Stop()
|
s.server.Stop()
|
||||||
s.client.Close()
|
s.client.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testGas = hexutil.Uint64(defaultGas + 1)
|
testGas = hexutil.Uint64(defaultGas + 1)
|
||||||
testGasPrice = (*hexutil.Big)(big.NewInt(10))
|
testGasPrice = (*hexutil.Big)(big.NewInt(10))
|
||||||
testOverridenGas = hexutil.Uint64(defaultGas + 2)
|
testNonce = hexutil.Uint64(10)
|
||||||
testOverridenGasPrice = (*hexutil.Big)(big.NewInt(20))
|
|
||||||
testNonce = hexutil.Uint64(10)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) setupTransactionPoolAPI(args SendTxArgs, returnNonce, resultNonce hexutil.Uint64, account *account.SelectedExtKey, txErr error, signArgs *sign.TxArgs) {
|
func (s *TransactorSuite) setupTransactionPoolAPI(args SendTxArgs, returnNonce, resultNonce hexutil.Uint64, account *account.SelectedExtKey, txErr error) {
|
||||||
// Expect calls to gas functions only if there are no user defined values.
|
// Expect calls to gas functions only if there are no user defined values.
|
||||||
// And also set the expected gas and gas price for RLP encoding the expected tx.
|
// And also set the expected gas and gas price for RLP encoding the expected tx.
|
||||||
var usedGas hexutil.Uint64
|
var usedGas hexutil.Uint64
|
||||||
var usedGasPrice *big.Int
|
var usedGasPrice *big.Int
|
||||||
s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), account.Address, gethrpc.PendingBlockNumber).Return(&returnNonce, nil)
|
s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), account.Address, gethrpc.PendingBlockNumber).Return(&returnNonce, nil)
|
||||||
if signArgs != nil && signArgs.GasPrice != nil {
|
if args.GasPrice == nil {
|
||||||
usedGasPrice = (*big.Int)(signArgs.GasPrice)
|
|
||||||
} else if args.GasPrice == nil {
|
|
||||||
usedGasPrice = (*big.Int)(testGasPrice)
|
usedGasPrice = (*big.Int)(testGasPrice)
|
||||||
s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil)
|
s.txServiceMock.EXPECT().GasPrice(gomock.Any()).Return(testGasPrice, nil)
|
||||||
} else {
|
} else {
|
||||||
usedGasPrice = (*big.Int)(args.GasPrice)
|
usedGasPrice = (*big.Int)(args.GasPrice)
|
||||||
}
|
}
|
||||||
if signArgs != nil && signArgs.Gas != nil {
|
if args.Gas == nil {
|
||||||
usedGas = *signArgs.Gas
|
|
||||||
} else if args.Gas == nil {
|
|
||||||
s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil)
|
s.txServiceMock.EXPECT().EstimateGas(gomock.Any(), gomock.Any()).Return(testGas, nil)
|
||||||
usedGas = testGas
|
usedGas = testGas
|
||||||
} else {
|
} else {
|
||||||
usedGas = *args.Gas
|
usedGas = *args.Gas
|
||||||
}
|
}
|
||||||
// Prepare the transaction anD RLP encode it.
|
// Prepare the transaction and RLP encode it.
|
||||||
data := s.rlpEncodeTx(args, s.nodeConfig, account, &resultNonce, usedGas, usedGasPrice)
|
data := s.rlpEncodeTx(args, s.nodeConfig, account, &resultNonce, usedGas, usedGasPrice)
|
||||||
// Expect the RLP encoded transaction.
|
// Expect the RLP encoded transaction.
|
||||||
s.txServiceMock.EXPECT().SendRawTransaction(gomock.Any(), data).Return(gethcommon.Hash{}, txErr)
|
s.txServiceMock.EXPECT().SendRawTransaction(gomock.Any(), data).Return(gethcommon.Hash{}, txErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfig, account *account.SelectedExtKey, nonce *hexutil.Uint64, gas hexutil.Uint64, gasPrice *big.Int) hexutil.Bytes {
|
func (s *TransactorSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfig, account *account.SelectedExtKey, nonce *hexutil.Uint64, gas hexutil.Uint64, gasPrice *big.Int) hexutil.Bytes {
|
||||||
newTx := types.NewTransaction(
|
newTx := types.NewTransaction(
|
||||||
uint64(*nonce),
|
uint64(*nonce),
|
||||||
*args.To,
|
*args.To,
|
||||||
|
@ -128,87 +116,36 @@ func (s *TxQueueTestSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfi
|
||||||
return hexutil.Bytes(data)
|
return hexutil.Bytes(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestCompleteTransaction() {
|
func (s *TransactorSuite) TestGasValues() {
|
||||||
key, _ := crypto.GenerateKey()
|
key, _ := crypto.GenerateKey()
|
||||||
selectedAccount := &account.SelectedExtKey{
|
selectedAccount := &account.SelectedExtKey{
|
||||||
Address: account.FromAddress(TestConfig.Account1.Address),
|
Address: account.FromAddress(TestConfig.Account1.Address),
|
||||||
AccountKey: &keystore.Key{PrivateKey: key},
|
AccountKey: &keystore.Key{PrivateKey: key},
|
||||||
}
|
}
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
gas *hexutil.Uint64
|
gas *hexutil.Uint64
|
||||||
gasPrice *hexutil.Big
|
gasPrice *hexutil.Big
|
||||||
signTxArgs *sign.TxArgs
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"noGasDef",
|
"noGasDef",
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
s.defaultSignTxArgs(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gasDefined",
|
"gasDefined",
|
||||||
&testGas,
|
&testGas,
|
||||||
nil,
|
nil,
|
||||||
s.defaultSignTxArgs(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gasPriceDefined",
|
"gasPriceDefined",
|
||||||
nil,
|
nil,
|
||||||
testGasPrice,
|
testGasPrice,
|
||||||
s.defaultSignTxArgs(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"inputPassedInLegacyDataField",
|
|
||||||
nil,
|
|
||||||
testGasPrice,
|
|
||||||
s.defaultSignTxArgs(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"overrideGas",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
&sign.TxArgs{
|
|
||||||
Gas: &testGas,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"overridePreExistingGas",
|
|
||||||
&testGas,
|
|
||||||
nil,
|
|
||||||
&sign.TxArgs{
|
|
||||||
Gas: &testOverridenGas,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"overridePreExistingGasPrice",
|
|
||||||
nil,
|
|
||||||
testGasPrice,
|
|
||||||
&sign.TxArgs{
|
|
||||||
GasPrice: testOverridenGasPrice,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nilSignTransactionSpecificArgs",
|
"nilSignTransactionSpecificArgs",
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"overridePreExistingGasWithNil",
|
|
||||||
&testGas,
|
|
||||||
nil,
|
|
||||||
&sign.TxArgs{
|
|
||||||
Gas: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"overridePreExistingGasPriceWithNil",
|
|
||||||
nil,
|
|
||||||
testGasPrice,
|
|
||||||
&sign.TxArgs{
|
|
||||||
GasPrice: nil,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,96 +158,48 @@ func (s *TxQueueTestSuite) TestCompleteTransaction() {
|
||||||
Gas: testCase.gas,
|
Gas: testCase.gas,
|
||||||
GasPrice: testCase.gasPrice,
|
GasPrice: testCase.gasPrice,
|
||||||
}
|
}
|
||||||
s.setupTransactionPoolAPI(args, testNonce, testNonce, selectedAccount, nil, testCase.signTxArgs)
|
s.setupTransactionPoolAPI(args, testNonce, testNonce, selectedAccount, nil)
|
||||||
|
|
||||||
w := make(chan struct{})
|
hash, err := s.manager.SendTransaction(args, selectedAccount)
|
||||||
var sendHash gethcommon.Hash
|
s.NoError(err)
|
||||||
go func() {
|
s.False(reflect.DeepEqual(hash, gethcommon.Hash{}))
|
||||||
var sendErr error
|
|
||||||
sendHash, sendErr = s.manager.SendTransaction(context.Background(), args)
|
|
||||||
s.NoError(sendErr)
|
|
||||||
close(w)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 10; i > 0; i-- {
|
|
||||||
if s.manager.pendingSignRequests.Count() > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := s.manager.pendingSignRequests.First()
|
|
||||||
s.NotNil(req)
|
|
||||||
approveResult := s.manager.pendingSignRequests.Approve(req.ID, "", testCase.signTxArgs, simpleVerifyFunc(selectedAccount))
|
|
||||||
s.NoError(approveResult.Error)
|
|
||||||
s.NoError(WaitClosed(w, time.Second))
|
|
||||||
|
|
||||||
// Transaction should be already removed from the queue.
|
|
||||||
s.False(s.manager.pendingSignRequests.Has(req.ID))
|
|
||||||
s.Equal(sendHash.Bytes(), approveResult.Response.Bytes())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) defaultSignTxArgs() *sign.TxArgs {
|
func (s *TransactorSuite) TestArgsValidation() {
|
||||||
return &sign.TxArgs{}
|
args := SendTxArgs{
|
||||||
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
|
Data: hexutil.Bytes([]byte{0x01, 0x02}),
|
||||||
|
Input: hexutil.Bytes([]byte{0x02, 0x01}),
|
||||||
|
}
|
||||||
|
s.False(args.Valid())
|
||||||
|
selectedAccount := &account.SelectedExtKey{
|
||||||
|
Address: account.FromAddress(TestConfig.Account1.Address),
|
||||||
|
}
|
||||||
|
_, err := s.manager.SendTransaction(args, selectedAccount)
|
||||||
|
s.EqualError(err, ErrInvalidSendTxArgs.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestAccountMismatch() {
|
func (s *TransactorSuite) TestAccountMismatch() {
|
||||||
|
args := SendTxArgs{
|
||||||
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// missing account
|
||||||
|
_, err = s.manager.SendTransaction(args, nil)
|
||||||
|
s.EqualError(err, account.ErrNoAccountSelected.Error())
|
||||||
|
|
||||||
|
// mismatched accounts
|
||||||
selectedAccount := &account.SelectedExtKey{
|
selectedAccount := &account.SelectedExtKey{
|
||||||
Address: account.FromAddress(TestConfig.Account2.Address),
|
Address: account.FromAddress(TestConfig.Account2.Address),
|
||||||
}
|
}
|
||||||
|
_, err = s.manager.SendTransaction(args, selectedAccount)
|
||||||
args := SendTxArgs{
|
s.EqualError(err, ErrInvalidTxSender.Error())
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
s.manager.SendTransaction(context.Background(), args) // nolint: errcheck
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 10; i > 0; i-- {
|
|
||||||
if s.manager.pendingSignRequests.Count() > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := s.manager.pendingSignRequests.First()
|
|
||||||
s.NotNil(req)
|
|
||||||
result := s.manager.pendingSignRequests.Approve(req.ID, "", s.defaultSignTxArgs(), simpleVerifyFunc(selectedAccount))
|
|
||||||
s.EqualError(result.Error, ErrInvalidCompleteTxSender.Error())
|
|
||||||
|
|
||||||
// Transaction should stay in the queue as mismatched accounts
|
|
||||||
// is a recoverable error.
|
|
||||||
s.True(s.manager.pendingSignRequests.Has(req.ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestDiscardTransaction() {
|
|
||||||
args := SendTxArgs{
|
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
|
||||||
}
|
|
||||||
w := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
_, err := s.manager.SendTransaction(context.Background(), args)
|
|
||||||
s.Equal(sign.ErrSignReqDiscarded, err)
|
|
||||||
close(w)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 10; i > 0; i-- {
|
|
||||||
if s.manager.pendingSignRequests.Count() > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := s.manager.pendingSignRequests.First()
|
|
||||||
s.NotNil(req)
|
|
||||||
err := s.manager.pendingSignRequests.Discard(req.ID)
|
|
||||||
s.NoError(err)
|
|
||||||
s.NoError(WaitClosed(w, time.Second))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLocalNonce verifies that local nonce will be used unless
|
// TestLocalNonce verifies that local nonce will be used unless
|
||||||
|
@ -320,7 +209,7 @@ func (s *TxQueueTestSuite) TestDiscardTransaction() {
|
||||||
// then, we return higher nonce, as if another node was used to send 2 transactions
|
// then, we return higher nonce, as if another node was used to send 2 transactions
|
||||||
// upstream nonce will be equal to 5, we update our local counter to 5+1
|
// upstream nonce will be equal to 5, we update our local counter to 5+1
|
||||||
// as the last step, we verify that if tx failed nonce is not updated
|
// as the last step, we verify that if tx failed nonce is not updated
|
||||||
func (s *TxQueueTestSuite) TestLocalNonce() {
|
func (s *TransactorSuite) TestLocalNonce() {
|
||||||
txCount := 3
|
txCount := 3
|
||||||
key, _ := crypto.GenerateKey()
|
key, _ := crypto.GenerateKey()
|
||||||
selectedAccount := &account.SelectedExtKey{
|
selectedAccount := &account.SelectedExtKey{
|
||||||
|
@ -329,30 +218,14 @@ func (s *TxQueueTestSuite) TestLocalNonce() {
|
||||||
}
|
}
|
||||||
nonce := hexutil.Uint64(0)
|
nonce := hexutil.Uint64(0)
|
||||||
|
|
||||||
go func() {
|
|
||||||
approved := 0
|
|
||||||
for {
|
|
||||||
// 3 in a cycle, then 2
|
|
||||||
if approved >= txCount+2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req := s.manager.pendingSignRequests.First()
|
|
||||||
if req == nil {
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
} else {
|
|
||||||
s.manager.pendingSignRequests.Approve(req.ID, "", s.defaultSignTxArgs(), simpleVerifyFunc(selectedAccount)) // nolint: errcheck
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 0; i < txCount; i++ {
|
for i := 0; i < txCount; i++ {
|
||||||
args := SendTxArgs{
|
args := SendTxArgs{
|
||||||
From: account.FromAddress(TestConfig.Account1.Address),
|
From: account.FromAddress(TestConfig.Account1.Address),
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
}
|
}
|
||||||
s.setupTransactionPoolAPI(args, nonce, hexutil.Uint64(i), selectedAccount, nil, nil)
|
s.setupTransactionPoolAPI(args, nonce, hexutil.Uint64(i), selectedAccount, nil)
|
||||||
|
|
||||||
_, err := s.manager.SendTransaction(context.Background(), args)
|
_, err := s.manager.SendTransaction(args, selectedAccount)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
resultNonce, _ := s.manager.localNonce.Load(args.From)
|
resultNonce, _ := s.manager.localNonce.Load(args.From)
|
||||||
s.Equal(uint64(i)+1, resultNonce.(uint64))
|
s.Equal(uint64(i)+1, resultNonce.(uint64))
|
||||||
|
@ -364,9 +237,9 @@ func (s *TxQueueTestSuite) TestLocalNonce() {
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
}
|
}
|
||||||
|
|
||||||
s.setupTransactionPoolAPI(args, nonce, nonce, selectedAccount, nil, nil)
|
s.setupTransactionPoolAPI(args, nonce, nonce, selectedAccount, nil)
|
||||||
|
|
||||||
_, err := s.manager.SendTransaction(context.Background(), args)
|
_, err := s.manager.SendTransaction(args, selectedAccount)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
|
||||||
resultNonce, _ := s.manager.localNonce.Load(args.From)
|
resultNonce, _ := s.manager.localNonce.Load(args.From)
|
||||||
|
@ -379,13 +252,13 @@ func (s *TxQueueTestSuite) TestLocalNonce() {
|
||||||
To: account.ToAddress(TestConfig.Account2.Address),
|
To: account.ToAddress(TestConfig.Account2.Address),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.manager.SendTransaction(context.Background(), args)
|
_, err = s.manager.SendTransaction(args, selectedAccount)
|
||||||
s.EqualError(testErr, err.Error())
|
s.EqualError(err, testErr.Error())
|
||||||
resultNonce, _ = s.manager.localNonce.Load(args.From)
|
resultNonce, _ = s.manager.localNonce.Load(args.From)
|
||||||
s.Equal(uint64(nonce)+1, resultNonce.(uint64))
|
s.Equal(uint64(nonce)+1, resultNonce.(uint64))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TxQueueTestSuite) TestContractCreation() {
|
func (s *TransactorSuite) TestContractCreation() {
|
||||||
key, _ := crypto.GenerateKey()
|
key, _ := crypto.GenerateKey()
|
||||||
testaddr := crypto.PubkeyToAddress(key.PublicKey)
|
testaddr := crypto.PubkeyToAddress(key.PublicKey)
|
||||||
genesis := core.GenesisAlloc{
|
genesis := core.GenesisAlloc{
|
||||||
|
@ -404,19 +277,7 @@ func (s *TxQueueTestSuite) TestContractCreation() {
|
||||||
Input: hexutil.Bytes(gethcommon.FromHex(contract.ENSBin)),
|
Input: hexutil.Bytes(gethcommon.FromHex(contract.ENSBin)),
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
hash, err := s.manager.SendTransaction(tx, selectedAccount)
|
||||||
for i := 1000; i > 0; i-- {
|
|
||||||
req := s.manager.pendingSignRequests.First()
|
|
||||||
if req == nil {
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
} else {
|
|
||||||
s.manager.pendingSignRequests.Approve(req.ID, "", s.defaultSignTxArgs(), simpleVerifyFunc(selectedAccount)) // nolint: errcheck
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
hash, err := s.manager.SendTransaction(context.Background(), tx)
|
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
backend.Commit()
|
backend.Commit()
|
||||||
receipt, err := backend.TransactionReceipt(context.TODO(), hash)
|
receipt, err := backend.TransactionReceipt(context.TODO(), hash)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package transactions
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
ethereum "github.com/ethereum/go-ethereum"
|
ethereum "github.com/ethereum/go-ethereum"
|
||||||
|
@ -13,12 +12,11 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrInvalidSendTxArgs is returned when the structure of SendTxArgs is ambigious.
|
// ErrInvalidSendTxArgs is returned when the structure of SendTxArgs is ambigious.
|
||||||
ErrInvalidSendTxArgs = errors.New("Transaction arguments are invalid (are both 'input' and 'data' fields used?)")
|
ErrInvalidSendTxArgs = errors.New("transaction arguments are invalid")
|
||||||
// ErrUnexpectedArgs returned when args are of unexpected length.
|
// ErrUnexpectedArgs is returned when args are of unexpected length.
|
||||||
ErrUnexpectedArgs = errors.New("unexpected args")
|
ErrUnexpectedArgs = errors.New("unexpected args")
|
||||||
|
//ErrInvalidTxSender is returned when selected account is different tham From field.
|
||||||
//ErrInvalidCompleteTxSender - error transaction with invalid sender
|
ErrInvalidTxSender = errors.New("transaction can only be send by its creator")
|
||||||
ErrInvalidCompleteTxSender = errors.New("transaction can only be completed by its creator")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PendingNonceProvider provides information about nonces.
|
// PendingNonceProvider provides information about nonces.
|
||||||
|
@ -72,20 +70,3 @@ func (args SendTxArgs) GetInput() hexutil.Bytes {
|
||||||
func isNilOrEmpty(bytes hexutil.Bytes) bool {
|
func isNilOrEmpty(bytes hexutil.Bytes) bool {
|
||||||
return bytes == nil || len(bytes) == 0
|
return bytes == nil || len(bytes) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// RPCCalltoSendTxArgs creates SendTxArgs based on RPC parameters
|
|
||||||
func RPCCalltoSendTxArgs(args ...interface{}) (SendTxArgs, error) {
|
|
||||||
var txArgs SendTxArgs
|
|
||||||
if len(args) != 1 {
|
|
||||||
return txArgs, ErrUnexpectedArgs
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return txArgs, err
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &txArgs); err != nil {
|
|
||||||
return txArgs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return txArgs, nil
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue