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:
Sebastian Delgado 2018-08-16 04:37:53 -07:00 committed by Adam Babik
parent ebc77374b2
commit 4afd9e6c6c
25 changed files with 410 additions and 2688 deletions

View File

@ -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()

View File

@ -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(&params.NodeConfig{}) err := backend.StartNode(&params.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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)), &params)
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)), &params)
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)), &params)
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

View File

@ -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{} {

64
lib/response.go Normal file
View File

@ -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)
}

27
lib/response_test.go Normal file
View File

@ -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"}}`)
}

View File

@ -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"`
}

View File

@ -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
} }

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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),
}
}

View File

@ -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,
}
}

View File

@ -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())
}
}
}
}

View File

@ -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)
}
} }

View File

@ -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,
} }

View File

@ -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)

View File

@ -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")
} }

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
}