From 4cc6028d59aae58f70ff139f55e2385b02d626ea Mon Sep 17 00:00:00 2001 From: Igor Mandrigin Date: Tue, 10 Apr 2018 12:02:54 +0200 Subject: [PATCH] Implement `personal_sign`. This commit implements `personal_sign` RPC or web3 personal.sign methods. NB! Contains breaking API changes. --- Gopkg.lock | 2 +- Gopkg.toml | 4 + Makefile | 1 + .../geth/0023-extract-personal-sign-api.patch | 61 ++++ cmd/statusd/debug/commands.go | 12 +- geth/api/api.go | 24 +- geth/api/backend.go | 39 ++- geth/node/node.go | 26 +- geth/params/defaults.go | 2 +- geth/rpc/client.go | 19 ++ geth/transactions/transactor.go | 25 +- geth/transactions/transactor_test.go | 18 +- geth/transactions/types.go | 3 + lib/library.go | 70 ++--- lib/library_test_utils.go | 52 ++-- lib/types.go | 20 +- services/personal/README.md | 7 + services/personal/api.go | 111 +++++++ services/personal/service.go | 51 ++++ {shhext => services/shhext}/README.md | 0 {shhext => services/shhext}/api.go | 0 {shhext => services/shhext}/service.go | 0 {shhext => services/shhext}/service_test.go | 0 {shhext => services/shhext}/signal.go | 0 sign/errors.go | 42 ++- sign/notifications.go | 79 ++--- sign/pending_requests.go | 40 +-- sign/pending_requests_test.go | 68 +++-- sign/request.go | 10 +- sign/result.go | 38 ++- t/e2e/api/backend_test.go | 18 +- t/e2e/jail/jail_rpc_test.go | 18 +- t/e2e/services/personal_sign_test.go | 221 ++++++++++++++ t/e2e/transactions/transactions_test.go | 279 ++++++++---------- t/e2e/whisper/whisper_ext_test.go | 2 +- .../go-ethereum/ethapi/private_account.go | 26 ++ .../go-ethereum/internal/ethapi/api.go | 10 +- 37 files changed, 986 insertions(+), 412 deletions(-) create mode 100644 _assets/patches/geth/0023-extract-personal-sign-api.patch create mode 100644 services/personal/README.md create mode 100644 services/personal/api.go create mode 100644 services/personal/service.go rename {shhext => services/shhext}/README.md (100%) rename {shhext => services/shhext}/api.go (100%) rename {shhext => services/shhext}/service.go (100%) rename {shhext => services/shhext}/service_test.go (100%) rename {shhext => services/shhext}/signal.go (100%) create mode 100644 t/e2e/services/personal_sign_test.go create mode 100644 vendor/github.com/ethereum/go-ethereum/ethapi/private_account.go diff --git a/Gopkg.lock b/Gopkg.lock index 056c341d1..2d5a62650 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -489,6 +489,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "eff125b3ad51a2875bc0ee5f9d190ce65ee9fb774e79d8df0de7a7e4bf4e32f8" + inputs-digest = "aee11795bbc6edceede45c682bef1deaf1c8166377c53632fc1fd23003e2a784" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 2f9a467f9..06598e153 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,3 +1,6 @@ +# These packages are introduced by patching go-ethereum, so there isn't any point in forcing dep to find them. +ignored = [ "github.com/ethereum/go-ethereum/ethapi" ] + [prune] unused-packages = true go-tests = true @@ -13,6 +16,7 @@ unused-packages = false non-go = false + # * * * * * constrained `status-go` dependencies * * * * * # (for the full dependency list see `Gopkg.lock`) diff --git a/Makefile b/Makefile index 1ed7ac9d9..94f3ef301 100644 --- a/Makefile +++ b/Makefile @@ -141,6 +141,7 @@ test-e2e: ##@tests Run e2e tests go test -timeout 20m ./t/e2e/rpc/... -network=$(networkid) $(gotest_extraflags) go test -timeout 20m ./t/e2e/whisper/... -network=$(networkid) $(gotest_extraflags) go test -timeout 10m ./t/e2e/transactions/... -network=$(networkid) $(gotest_extraflags) + go test -timeout 10m ./t/e2e/services/... -network=$(networkid) $(gotest_extraflags) # e2e_test tag is required to include some files from ./lib without _test suffix go test -timeout 40m -tags e2e_test ./lib -network=$(networkid) $(gotest_extraflags) diff --git a/_assets/patches/geth/0023-extract-personal-sign-api.patch b/_assets/patches/geth/0023-extract-personal-sign-api.patch new file mode 100644 index 000000000..f3d3cf3f9 --- /dev/null +++ b/_assets/patches/geth/0023-extract-personal-sign-api.patch @@ -0,0 +1,61 @@ +diff --git a/ethapi/private_account.go b/ethapi/private_account.go +new file mode 100644 +index 00000000..8d51fd31 +--- /dev/null ++++ b/ethapi/private_account.go +@@ -0,0 +1,26 @@ ++package ethapi ++ ++import ( ++ "context" ++ ++ "github.com/ethereum/go-ethereum/accounts" ++ "github.com/ethereum/go-ethereum/common" ++ "github.com/ethereum/go-ethereum/common/hexutil" ++ "github.com/ethereum/go-ethereum/internal/ethapi" ++) ++ ++type LimitedPersonalAPI struct { ++ privateAPI *ethapi.PrivateAccountAPI ++} ++ ++func NewLimitedPersonalAPI(am *accounts.Manager) *LimitedPersonalAPI { ++ return &LimitedPersonalAPI{ethapi.NewSubsetOfPrivateAccountAPI(am)} ++} ++ ++func (s *LimitedPersonalAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) { ++ return s.privateAPI.Sign(ctx, data, addr, passwd) ++} ++ ++func (s *LimitedPersonalAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) { ++ return s.privateAPI.EcRecover(ctx, data, sig) ++} +diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go +index 31408633..3cee8753 100644 +--- a/internal/ethapi/api.go ++++ b/internal/ethapi/api.go +@@ -214,6 +214,14 @@ func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI { + } + } + ++func NewSubsetOfPrivateAccountAPI(am *accounts.Manager) *PrivateAccountAPI { ++ return &PrivateAccountAPI{ ++ am: am, ++ nonceLock: nil, ++ b: nil, ++ } ++} ++ + // ListAccounts will return a list of addresses for accounts this node manages. + func (s *PrivateAccountAPI) ListAccounts() []common.Address { + addresses := make([]common.Address, 0) // return [] instead of nil if empty +@@ -426,7 +434,7 @@ func (s *PrivateAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr c + // Look up the wallet containing the requested signer + account := accounts.Account{Address: addr} + +- wallet, err := s.b.AccountManager().Find(account) ++ wallet, err := s.am.Find(account) + if err != nil { + return nil, err + } + diff --git a/cmd/statusd/debug/commands.go b/cmd/statusd/debug/commands.go index 7bd694ea8..98dbd099b 100644 --- a/cmd/statusd/debug/commands.go +++ b/cmd/statusd/debug/commands.go @@ -167,11 +167,11 @@ func (cs *commandSet) Logout() error { return cs.statusAPI.Logout() } -// CompleteTransaction instructs API to complete sending of a given transaction. -func (cs *commandSet) CompleteTransaction(id, password string) (string, error) { - txHash, err := cs.statusAPI.CompleteTransaction(id, password) - if err != nil { - return "", err +// ApproveSignRequest instructs API to complete sending of a given transaction. +func (cs *commandSet) ApproveSignRequest(id, password string) (string, error) { + result := cs.statusAPI.ApproveSignRequest(id, password) + if result.Error != nil { + return "", result.Error } - return txHash.String(), nil + return result.Response.Hex(), nil } diff --git a/geth/api/api.go b/geth/api/api.go index 61768cad3..46f80ccbf 100644 --- a/geth/api/api.go +++ b/geth/api/api.go @@ -161,24 +161,24 @@ func (api *StatusAPI) SendTransaction(ctx context.Context, args transactions.Sen return api.b.SendTransaction(ctx, args) } -// CompleteTransaction instructs backend to complete sending of a given transaction -func (api *StatusAPI) CompleteTransaction(id string, password string) (gethcommon.Hash, error) { - return api.b.CompleteTransaction(id, password) +// ApproveSignRequest instructs backend to complete sending of a given transaction +func (api *StatusAPI) ApproveSignRequest(id string, password string) sign.Result { + return api.b.ApproveSignRequest(id, password) } -// CompleteTransactions instructs backend to complete sending of multiple transactions -func (api *StatusAPI) CompleteTransactions(ids []string, password string) map[string]sign.Result { - return api.b.CompleteTransactions(ids, password) +// ApproveSignRequests instructs backend to complete sending of multiple transactions +func (api *StatusAPI) ApproveSignRequests(ids []string, password string) map[string]sign.Result { + return api.b.ApproveSignRequests(ids, password) } -// DiscardTransaction discards a given transaction from transaction queue -func (api *StatusAPI) DiscardTransaction(id string) error { - return api.b.DiscardTransaction(id) +// DiscardSignRequest discards a given transaction from transaction queue +func (api *StatusAPI) DiscardSignRequest(id string) error { + return api.b.DiscardSignRequest(id) } -// DiscardTransactions discards given multiple transactions from transaction queue -func (api *StatusAPI) DiscardTransactions(ids []string) map[string]error { - return api.b.DiscardTransactions(ids) +// DiscardSignRequests discards given multiple transactions from transaction queue +func (api *StatusAPI) DiscardSignRequests(ids []string) map[string]error { + return api.b.DiscardSignRequests(ids) } // JailParse creates a new jail cell context, with the given chatID as identifier. diff --git a/geth/api/backend.go b/geth/api/backend.go index 48a0880f4..8d982c75c 100644 --- a/geth/api/backend.go +++ b/geth/api/backend.go @@ -14,8 +14,10 @@ import ( "github.com/status-im/status-go/geth/node" "github.com/status-im/status-go/geth/notifications/push/fcm" "github.com/status-im/status-go/geth/params" + "github.com/status-im/status-go/geth/rpc" "github.com/status-im/status-go/geth/signal" "github.com/status-im/status-go/geth/transactions" + "github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/sign" ) @@ -36,6 +38,7 @@ type StatusBackend struct { mu sync.Mutex statusNode *node.StatusNode pendingSignRequests *sign.PendingRequests + personalAPI *personal.PublicAPI accountManager *account.Manager transactor *transactions.Transactor jailManager jail.Manager @@ -52,6 +55,7 @@ func NewStatusBackend() *StatusBackend { pendingSignRequests := sign.NewPendingRequests() accountManager := account.NewManager(statusNode) transactor := transactions.NewTransactor(pendingSignRequests) + personalAPI := personal.NewAPI(pendingSignRequests) jailManager := jail.New(statusNode) notificationManager := fcm.NewNotification(fcmServerKey) @@ -61,6 +65,7 @@ func NewStatusBackend() *StatusBackend { accountManager: accountManager, jailManager: jailManager, transactor: transactor, + personalAPI: personalAPI, newNotification: notificationManager, log: log.New("package", "status-go/geth/api.StatusBackend"), } @@ -81,6 +86,11 @@ func (b *StatusBackend) JailManager() jail.Manager { return b.jailManager } +// PersonalAPI returns reference to personal APIs +func (b *StatusBackend) PersonalAPI() *personal.PublicAPI { + return b.personalAPI +} + // Transactor returns reference to a status transactor func (b *StatusBackend) Transactor() *transactions.Transactor { return b.transactor @@ -123,7 +133,8 @@ func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) { signal.Send(signal.Envelope{Type: signal.EventNodeStarted}) b.transactor.SetNetworkID(config.NetworkID) - b.transactor.SetRPCClient(b.statusNode.RPCClient()) + b.transactor.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout) + b.personalAPI.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout) if err := b.registerHandlers(); err != nil { b.log.Error("Handler registration failed", "err", err) } @@ -212,34 +223,30 @@ func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedEx return selectedAccount, nil } -// CompleteTransaction instructs backend to complete sending of a given transaction -func (b *StatusBackend) CompleteTransaction(id string, password string) (hash gethcommon.Hash, err error) { +// ApproveSignRequest instructs backend to complete sending of a given transaction +func (b *StatusBackend) ApproveSignRequest(id string, password string) sign.Result { return b.pendingSignRequests.Approve(id, password, b.getVerifiedAccount) } -// CompleteTransactions instructs backend to complete sending of multiple transactions -func (b *StatusBackend) CompleteTransactions(ids []string, password string) map[string]sign.Result { +// 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 { - txHash, txErr := b.CompleteTransaction(txID, password) - results[txID] = sign.Result{ - Hash: txHash, - Error: txErr, - } + results[txID] = b.ApproveSignRequest(txID, password) } return results } -// DiscardTransaction discards a given transaction from transaction queue -func (b *StatusBackend) DiscardTransaction(id string) error { +// DiscardSignRequest discards a given transaction from transaction queue +func (b *StatusBackend) DiscardSignRequest(id string) error { return b.pendingSignRequests.Discard(id) } -// DiscardTransactions discards given multiple transactions from transaction queue -func (b *StatusBackend) DiscardTransactions(ids []string) map[string]error { +// 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.DiscardTransaction(txID) + err := b.DiscardSignRequest(txID) if err != nil { results[txID] = err } @@ -273,6 +280,8 @@ func (b *StatusBackend) registerHandlers() error { return hash.Hex(), err }) + rpcClient.RegisterHandler(params.PersonalSignMethodName, b.PersonalAPI().Sign) + return nil } diff --git a/geth/node/node.go b/geth/node/node.go index d1f6cd890..31007b93d 100644 --- a/geth/node/node.go +++ b/geth/node/node.go @@ -24,14 +24,16 @@ import ( "github.com/status-im/status-go/geth/mailservice" "github.com/status-im/status-go/geth/params" shhmetrics "github.com/status-im/status-go/metrics/whisper" - "github.com/status-im/status-go/shhext" + "github.com/status-im/status-go/services/personal" + "github.com/status-im/status-go/services/shhext" ) // Errors related to node and services creation. var ( - ErrNodeMakeFailure = errors.New("error creating p2p node") - ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service") - ErrLightEthRegistrationFailure = errors.New("failed to register the LES service") + ErrNodeMakeFailure = errors.New("error creating p2p node") + ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service") + ErrLightEthRegistrationFailure = errors.New("failed to register the LES service") + ErrPersonalServiceRegistrationFailure = errors.New("failed to register the personal api service") ) // All general log messages in this package should be routed through this logger. @@ -76,6 +78,15 @@ func MakeNode(config *params.NodeConfig) (*node.Node, error) { if err := activateLightEthService(stack, config); err != nil { return nil, fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err) } + } else { + // `personal_sign` and `personal_recover` methods are important to + // keep DApps working. + // Usually, they are provided by an ETH or a LES service, but when using + // upstream, we don't start any of these, so we need to start our own + // implementation. + if err := activatePersonalService(stack, config); err != nil { + return nil, fmt.Errorf("%v: %v", ErrPersonalServiceRegistrationFailure, err) + } } // start Whisper service. @@ -148,6 +159,13 @@ func activateLightEthService(stack *node.Node, config *params.NodeConfig) error }) } +func activatePersonalService(stack *node.Node, config *params.NodeConfig) error { + return stack.Register(func(*node.ServiceContext) (node.Service, error) { + svc := personal.New(stack.AccountManager()) + return svc, nil + }) +} + // activateShhService configures Whisper and adds it to the given node. func activateShhService(stack *node.Node, config *params.NodeConfig) (err error) { if config.WhisperConfig == nil || !config.WhisperConfig.Enabled { diff --git a/geth/params/defaults.go b/geth/params/defaults.go index 1f2125f15..13aeb7ce6 100644 --- a/geth/params/defaults.go +++ b/geth/params/defaults.go @@ -30,7 +30,7 @@ const ( ListenAddr = ":0" // APIModules is a list of modules to expose via any type of RPC (HTTP, IPC, in-proc) - APIModules = "eth,net,web3,shh,shhext" + APIModules = "eth,net,web3,shh,shhext,personal" // SendTransactionMethodName defines the name for a giving transaction. SendTransactionMethodName = "eth_sendTransaction" diff --git a/geth/rpc/client.go b/geth/rpc/client.go index 262152a1b..34435cdf6 100644 --- a/geth/rpc/client.go +++ b/geth/rpc/client.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "sync" + "time" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/geth/params" @@ -14,6 +15,11 @@ import ( gethrpc "github.com/ethereum/go-ethereum/rpc" ) +const ( + // DefaultCallTimeout is a default timeout for an RPC call + DefaultCallTimeout = time.Minute +) + // Handler defines handler for RPC methods. type Handler func(context.Context, ...interface{}) (interface{}, error) @@ -81,15 +87,28 @@ func (c *Client) Call(result interface{}, method string, args ...interface{}) er // can also pass nil, in which case the result is ignored. // // It uses custom routing scheme for calls. +// If there are any local handlers registered for this call, they will handle it. func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { // check locally registered handlers first if handler, ok := c.handler(method); ok { return c.callMethod(ctx, result, handler, args...) } + return c.CallContextIgnoringLocalHandlers(ctx, result, method, args...) +} + +// CallContextIgnoringLocalHandlers performs a JSON-RPC call with the given +// arguments. +// +// If there are local handlers registered for this call, they would +// be ignored. It is useful if the call is happening from within a local +// handler itself. +// Upstream calls routing will be used anyway. +func (c *Client) CallContextIgnoringLocalHandlers(ctx context.Context, result interface{}, method string, args ...interface{}) error { if c.router.routeRemote(method) { return c.upstream.CallContext(ctx, result, method, args...) } + return c.local.CallContext(ctx, result, method, args...) } diff --git a/geth/transactions/transactor.go b/geth/transactions/transactor.go index fb5952f66..1962a888e 100644 --- a/geth/transactions/transactor.go +++ b/geth/transactions/transactor.go @@ -13,14 +13,14 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/geth/account" + "github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/geth/rpc" "github.com/status-im/status-go/sign" ) const ( // sendTxTimeout defines how many seconds to wait before returning result in sentTransaction(). - sendTxTimeout = 300 * time.Second - rpcCallTimeout = time.Minute + sendTxTimeout = 300 * time.Second defaultGas = 90000 ) @@ -47,7 +47,6 @@ func NewTransactor(signRequests *sign.PendingRequests) *Transactor { pendingSignRequests: signRequests, addrLock: &AddrLocker{}, sendTxTimeout: sendTxTimeout, - rpcCallTimeout: rpcCallTimeout, localNonce: sync.Map{}, log: log.New("package", "status-go/geth/transactions.Manager"), } @@ -58,12 +57,13 @@ func (t *Transactor) SetNetworkID(networkID uint64) { t.networkID = networkID } -// SetRPCClient an RPC client. -func (t *Transactor) SetRPCClient(rpcClient *rpc.Client) { +// SetRPC sets RPC params, a client and a timeout +func (t *Transactor) SetRPC(rpcClient *rpc.Client, timeout time.Duration) { rpcWrapper := newRPCWrapper(rpcClient) t.sender = rpcWrapper t.pendingNonceProvider = rpcWrapper t.gasCalculator = rpcWrapper + t.rpcCallTimeout = timeout } // SendTransaction is an implementation of eth_sendTransaction. It queues the tx to the sign queue. @@ -72,18 +72,19 @@ func (t *Transactor) SendTransaction(ctx context.Context, args SendTxArgs) (geth ctx = context.Background() } - completeFunc := func(acc *account.SelectedExtKey) (gethcommon.Hash, error) { - return t.validateAndPropagate(acc, args) + completeFunc := func(acc *account.SelectedExtKey, password string) (sign.Response, error) { + hash, err := t.validateAndPropagate(acc, args) + return sign.Response(hash.Bytes()), err } - request, err := t.pendingSignRequests.Add(ctx, args, completeFunc) + request, err := t.pendingSignRequests.Add(ctx, params.SendTransactionMethodName, args, completeFunc) if err != nil { return gethcommon.Hash{}, err } - rst := t.pendingSignRequests.Wait(request.ID, t.sendTxTimeout) - return rst.Hash, rst.Error + 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 @@ -93,7 +94,7 @@ func (t *Transactor) validateAccount(args SendTxArgs, selectedAccount *account.S } if args.From.Hex() != selectedAccount.Address.Hex() { - err := sign.ErrInvalidCompleteTxSender + err := sign.NewTransientError(ErrInvalidCompleteTxSender) t.log.Error("queued transaction does not belong to the selected account", "err", err) return err } @@ -102,8 +103,6 @@ func (t *Transactor) validateAccount(args SendTxArgs, selectedAccount *account.S } func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKey, args SendTxArgs) (hash gethcommon.Hash, err error) { - // TODO (mandrigin): Send sign request ID as a parameter to this function and uncoment the log message - // m.log.Info("complete transaction", "id", queuedTx.ID) if err := t.validateAccount(args, selectedAccount); err != nil { return hash, err } diff --git a/geth/transactions/transactor_test.go b/geth/transactions/transactor_test.go index c4664b449..4f8e56af7 100644 --- a/geth/transactions/transactor_test.go +++ b/geth/transactions/transactor_test.go @@ -65,9 +65,8 @@ func (s *TxQueueTestSuite) SetupTest() { s.manager = NewTransactor(sign.NewPendingRequests()) s.manager.sendTxTimeout = time.Second - s.manager.rpcCallTimeout = time.Second s.manager.SetNetworkID(chainID) - s.manager.SetRPCClient(rpcClient) + s.manager.SetRPC(rpcClient, time.Second) } func (s *TxQueueTestSuite) TearDownTest() { @@ -168,10 +167,7 @@ func (s *TxQueueTestSuite) TestCompleteTransaction() { s.setupTransactionPoolAPI(args, testNonce, testNonce, selectedAccount, nil) w := make(chan struct{}) - var ( - sendHash gethcommon.Hash - err error - ) + var sendHash gethcommon.Hash go func() { var sendErr error sendHash, sendErr = s.manager.SendTransaction(context.Background(), args) @@ -188,13 +184,13 @@ func (s *TxQueueTestSuite) TestCompleteTransaction() { req := s.manager.pendingSignRequests.First() s.NotNil(req) - approveHash, err := s.manager.pendingSignRequests.Approve(req.ID, "", simpleVerifyFunc(selectedAccount)) - s.NoError(err) + approveResult := s.manager.pendingSignRequests.Approve(req.ID, "", 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, approveHash) + s.Equal(sendHash.Bytes(), approveResult.Response.Bytes()) }) } } @@ -222,8 +218,8 @@ func (s *TxQueueTestSuite) TestAccountMismatch() { req := s.manager.pendingSignRequests.First() s.NotNil(req) - _, err := s.manager.pendingSignRequests.Approve(req.ID, "", simpleVerifyFunc(selectedAccount)) - s.Equal(err, sign.ErrInvalidCompleteTxSender) + result := s.manager.pendingSignRequests.Approve(req.ID, "", simpleVerifyFunc(selectedAccount)) + s.EqualError(result.Error, ErrInvalidCompleteTxSender.Error()) // Transaction should stay in the queue as mismatched accounts // is a recoverable error. diff --git a/geth/transactions/types.go b/geth/transactions/types.go index f1f02af4b..2081c3ec6 100644 --- a/geth/transactions/types.go +++ b/geth/transactions/types.go @@ -16,6 +16,9 @@ var ( ErrInvalidSendTxArgs = errors.New("Transaction arguments are invalid (are both 'input' and 'data' fields used?)") // ErrUnexpectedArgs returned when args are of unexpected length. ErrUnexpectedArgs = errors.New("unexpected args") + + //ErrInvalidCompleteTxSender - error transaction with invalid sender + ErrInvalidCompleteTxSender = errors.New("transaction can only be completed by its creator") ) // PendingNonceProvider provides information about nonces. diff --git a/lib/library.go b/lib/library.go index 0a69516df..15e818538 100644 --- a/lib/library.go +++ b/lib/library.go @@ -200,40 +200,40 @@ func Logout() *C.char { return makeJSONResponse(err) } -//CompleteTransaction instructs backend to complete sending of a given transaction -//export CompleteTransaction -func CompleteTransaction(id, password *C.char) *C.char { - txHash, err := statusAPI.CompleteTransaction(C.GoString(id), C.GoString(password)) +//ApproveSignRequest instructs backend to complete sending of a given transaction +//export ApproveSignRequest +func ApproveSignRequest(id, password *C.char) *C.char { + result := statusAPI.ApproveSignRequest(C.GoString(id), C.GoString(password)) errString := "" - if err != nil { - fmt.Fprintln(os.Stderr, err) - errString = err.Error() + if result.Error != nil { + fmt.Fprintln(os.Stderr, result.Error) + errString = result.Error.Error() } - out := CompleteTransactionResult{ + out := SignRequestResult{ ID: C.GoString(id), - Hash: txHash.Hex(), + Hash: result.Response.Hex(), Error: errString, } outBytes, err := json.Marshal(out) if err != nil { - logger.Error("failed to marshal CompleteTransaction output", "error", err) + logger.Error("failed to marshal ApproveSignRequest output", "error", err) return makeJSONResponse(err) } return C.CString(string(outBytes)) } -//CompleteTransactions instructs backend to complete sending of multiple transactions -//export CompleteTransactions -func CompleteTransactions(ids, password *C.char) *C.char { - out := CompleteTransactionsResult{} - out.Results = make(map[string]CompleteTransactionResult) +//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"] = CompleteTransactionResult{ + out.Results["none"] = SignRequestResult{ Error: err.Error(), } } else { @@ -242,11 +242,11 @@ func CompleteTransactions(ids, password *C.char) *C.char { txIDs[i] = id } - results := statusAPI.CompleteTransactions(txIDs, C.GoString(password)) + results := statusAPI.ApproveSignRequests(txIDs, C.GoString(password)) for txID, result := range results { - txResult := CompleteTransactionResult{ + txResult := SignRequestResult{ ID: txID, - Hash: result.Hash.Hex(), + Hash: result.Response.Hex(), } if result.Error != nil { txResult.Error = result.Error.Error() @@ -257,17 +257,17 @@ func CompleteTransactions(ids, password *C.char) *C.char { outBytes, err := json.Marshal(out) if err != nil { - logger.Error("failed to marshal CompleteTransactions output", "error", err) + logger.Error("failed to marshal ApproveSignRequests output", "error", err) return makeJSONResponse(err) } return C.CString(string(outBytes)) } -//DiscardTransaction discards a given transaction from transaction queue -//export DiscardTransaction -func DiscardTransaction(id *C.char) *C.char { - err := statusAPI.DiscardTransaction(C.GoString(id)) +//DiscardSignRequest discards a given transaction from transaction queue +//export DiscardSignRequest +func DiscardSignRequest(id *C.char) *C.char { + err := statusAPI.DiscardSignRequest(C.GoString(id)) errString := "" if err != nil { @@ -275,28 +275,28 @@ func DiscardTransaction(id *C.char) *C.char { errString = err.Error() } - out := DiscardTransactionResult{ + out := DiscardSignRequestResult{ ID: C.GoString(id), Error: errString, } outBytes, err := json.Marshal(out) if err != nil { - log.Error("failed to marshal DiscardTransaction output", "error", err) + log.Error("failed to marshal DiscardSignRequest output", "error", err) return makeJSONResponse(err) } return C.CString(string(outBytes)) } -//DiscardTransactions discards given multiple transactions from transaction queue -//export DiscardTransactions -func DiscardTransactions(ids *C.char) *C.char { - out := DiscardTransactionsResult{} - out.Results = make(map[string]DiscardTransactionResult) +//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"] = DiscardTransactionResult{ + out.Results["none"] = DiscardSignRequestResult{ Error: err.Error(), } } else { @@ -305,9 +305,9 @@ func DiscardTransactions(ids *C.char) *C.char { txIDs[i] = id } - results := statusAPI.DiscardTransactions(txIDs) + results := statusAPI.DiscardSignRequests(txIDs) for txID, err := range results { - out.Results[txID] = DiscardTransactionResult{ + out.Results[txID] = DiscardSignRequestResult{ ID: txID, Error: err.Error(), } @@ -316,7 +316,7 @@ func DiscardTransactions(ids *C.char) *C.char { outBytes, err := json.Marshal(out) if err != nil { - logger.Error("failed to marshal DiscardTransactions output", "error", err) + logger.Error("failed to marshal DiscardSignRequests output", "error", err) return makeJSONResponse(err) } diff --git a/lib/library_test_utils.go b/lib/library_test_utils.go index cea595bfa..df6fa2cbd 100644 --- a/lib/library_test_utils.go +++ b/lib/library_test_utils.go @@ -39,14 +39,16 @@ import ( . "github.com/status-im/status-go/t/utils" //nolint: golint ) -const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000" const initJS = ` var _status_catalog = { foo: 'bar' };` -var testChainDir string -var nodeConfigJSON string +var ( + zeroHash = sign.EmptyResponse.Hex() + testChainDir string + nodeConfigJSON string +) func init() { testChainDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()]) @@ -822,12 +824,12 @@ func testCompleteTransaction(t *testing.T) bool { t.Errorf("cannot unmarshal event's JSON: %s. Error %q", jsonEvent, err) return } - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string)) - completeTxResponse := CompleteTransactionResult{} - rawResponse := CompleteTransaction(C.CString(event["id"].(string)), C.CString(TestConfig.Account1.Password)) + 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) @@ -899,7 +901,7 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyc t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) return } - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.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) @@ -938,8 +940,8 @@ func testCompleteMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyc updatedTxIDStrings, _ := json.Marshal(parsedIDs) // complete - resultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password)) - resultsStruct := CompleteTransactionsResult{} + 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 @@ -1031,7 +1033,7 @@ func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) return } - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) txID = event["id"].(string) t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) @@ -1042,8 +1044,8 @@ func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo } // discard - discardResponse := DiscardTransactionResult{} - rawResponse := DiscardTransaction(C.CString(txID)) + 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) @@ -1055,7 +1057,7 @@ func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo } // try completing discarded transaction - _, err := statusAPI.CompleteTransaction(string(txID), TestConfig.Account1.Password) + err := statusAPI.ApproveSignRequest(string(txID), TestConfig.Account1.Password).Error if err != sign.ErrSignReqNotFound { t.Error("expects tx not found, but call to CompleteTransaction succeeded") return @@ -1069,7 +1071,7 @@ func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo completeQueuedTransaction <- struct{}{} // so that timeout is aborted } - if envelope.Type == sign.EventTransactionFailed { + if envelope.Type == sign.EventSignRequestFailed { event := envelope.Event.(map[string]interface{}) t.Logf("transaction return event received: {id: %s}\n", event["id"].(string)) @@ -1081,7 +1083,7 @@ func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo } receivedErrCode := event["error_code"].(string) - if receivedErrCode != strconv.Itoa(sign.SendTransactionDiscardedErrorCode) { + if receivedErrCode != strconv.Itoa(sign.SignRequestDiscardedErrorCode) { t.Errorf("unexpected error code received: got %v", receivedErrCode) return } @@ -1143,7 +1145,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocycl t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent) return } - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) txID = event["id"].(string) t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) @@ -1156,7 +1158,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocycl txIDs <- txID } - if envelope.Type == sign.EventTransactionFailed { + if envelope.Type == sign.EventSignRequestFailed { event := envelope.Event.(map[string]interface{}) t.Logf("transaction return event received: {id: %s}\n", event["id"].(string)) @@ -1168,7 +1170,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocycl } receivedErrCode := event["error_code"].(string) - if receivedErrCode != strconv.Itoa(sign.SendTransactionDiscardedErrorCode) { + if receivedErrCode != strconv.Itoa(sign.SignRequestDiscardedErrorCode) { t.Errorf("unexpected error code received: got %v", receivedErrCode) return } @@ -1210,8 +1212,8 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocycl updatedTxIDStrings, _ := json.Marshal(parsedIDs) // discard - discardResultsString := DiscardTransactions(C.CString(string(updatedTxIDStrings))) - discardResultsStruct := DiscardTransactionsResult{} + discardResultsString := DiscardSignRequests(C.CString(string(updatedTxIDStrings))) + discardResultsStruct := DiscardSignRequestsResult{} if err := json.Unmarshal([]byte(C.GoString(discardResultsString)), &discardResultsStruct); err != nil { t.Error(err) return @@ -1224,8 +1226,8 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocycl } // try completing discarded transaction - completeResultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password)) - completeResultsStruct := CompleteTransactionsResult{} + 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 @@ -1233,7 +1235,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocycl completeResults := completeResultsStruct.Results if len(completeResults) != (testTxCount + 1) { - t.Error("unexpected number of errors (call to CompleteTransaction should not succeed)") + t.Error("unexpected number of errors (call to ApproveSignRequest should not succeed)") } for txID, txResult := range completeResults { if txID != txResult.ID { @@ -1245,7 +1247,7 @@ func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocycl return } if txResult.Hash != zeroHash { - t.Errorf("invalid hash (expected zero): %s", txResult.Hash) + t.Errorf("invalid hash (expected zero): '%s'", txResult.Hash) return } } @@ -1457,7 +1459,7 @@ func startTestNode(t *testing.T) <-chan struct{} { return } - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { } if envelope.Type == signal.EventNodeStarted { t.Log("Node started, but we wait till it be ready") diff --git a/lib/types.go b/lib/types.go index 97d43b64a..f5db775b7 100644 --- a/lib/types.go +++ b/lib/types.go @@ -76,25 +76,25 @@ type NotifyResult struct { Error string `json:"error,omitempty"` } -// CompleteTransactionResult is a JSON returned from transaction complete function (used in exposed method) -type CompleteTransactionResult struct { +// 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"` } -// CompleteTransactionsResult is list of results from CompleteTransactions() (used in exposed method) -type CompleteTransactionsResult struct { - Results map[string]CompleteTransactionResult `json:"results"` +// SignRequestsResult is list of results from CompleteTransactions() (used in exposed method) +type SignRequestsResult struct { + Results map[string]SignRequestResult `json:"results"` } -// DiscardTransactionResult is a JSON returned from transaction discard function -type DiscardTransactionResult struct { +// DiscardSignRequestResult is a JSON returned from transaction discard function +type DiscardSignRequestResult struct { ID string `json:"id"` Error string `json:"error"` } -// DiscardTransactionsResult is a list of results from DiscardTransactions() -type DiscardTransactionsResult struct { - Results map[string]DiscardTransactionResult `json:"results"` +// DiscardSignRequestsResult is a list of results from DiscardTransactions() +type DiscardSignRequestsResult struct { + Results map[string]DiscardSignRequestResult `json:"results"` } diff --git a/services/personal/README.md b/services/personal/README.md new file mode 100644 index 000000000..dd912f138 --- /dev/null +++ b/services/personal/README.md @@ -0,0 +1,7 @@ +# personal + +This package contains Status integraton with `personal_*` RPC APIs more +information on these APIs can be found on the Ethereum Wiki: +https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal + +In `web3.js` these methods are located in `web3.personal` namespace. diff --git a/services/personal/api.go b/services/personal/api.go new file mode 100644 index 000000000..e429740c4 --- /dev/null +++ b/services/personal/api.go @@ -0,0 +1,111 @@ +package personal + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/status-im/status-go/geth/account" + "github.com/status-im/status-go/geth/params" + "github.com/status-im/status-go/geth/rpc" + "github.com/status-im/status-go/sign" +) + +var ( + // ErrInvalidPersonalSignAccount is returned when the account passed to + // personal_sign isn't equal to the currently selected account. + 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 { + Data interface{} `json:"data"` + Address string `json:"account"` +} + +func newMetadata(rpcParams []interface{}) (*metadata, error) { + // personal_sign can be called with the following parameters + // 1) data to sign + // 2) account + // 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. +type PublicAPI struct { + pendingSignRequests *sign.PendingRequests + rpcClient *rpc.Client + rpcTimeout time.Duration +} + +// NewAPI creates an instance of the personal API. +func NewAPI(pendingSignRequests *sign.PendingRequests) *PublicAPI { + return &PublicAPI{ + pendingSignRequests: pendingSignRequests, + } +} + +// SetRPC sets RPC params (client and timeout) for the API calls. +func (api *PublicAPI) SetRPC(rpcClient *rpc.Client, timeout time.Duration) { + api.rpcClient = rpcClient + api.rpcTimeout = timeout +} + +// Sign is an implementation of `personal_sign` or `web3.personal.sign` API +func (api *PublicAPI) Sign(context context.Context, rpcParams ...interface{}) (interface{}, error) { + metadata, err := newMetadata(rpcParams) + if err != nil { + 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) (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 + } +} + +// make sure that only account which created the tx can complete it +func (api *PublicAPI) validateAccount(metadata metadata, selectedAccount *account.SelectedExtKey) error { + if selectedAccount == nil { + return account.ErrNoAccountSelected + } + + // case-insensitive string comparison + if !strings.EqualFold(metadata.Address, selectedAccount.Address.Hex()) { + return ErrInvalidPersonalSignAccount + } + + return nil +} diff --git a/services/personal/service.go b/services/personal/service.go new file mode 100644 index 000000000..f5cf99790 --- /dev/null +++ b/services/personal/service.go @@ -0,0 +1,51 @@ +package personal + +import ( + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/ethapi" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rpc" +) + +// Make sure that Service implements node.Service interface. +var _ node.Service = (*Service)(nil) + +// Service represents out own implementation of personal sign operations. +type Service struct { + am *accounts.Manager +} + +// New returns a new Service. +func New(am *accounts.Manager) *Service { + return &Service{am} +} + +// Protocols returns a new protocols list. In this case, there are none. +func (s *Service) Protocols() []p2p.Protocol { + return []p2p.Protocol{} +} + +// APIs returns a list of new APIs. +func (s *Service) APIs() []rpc.API { + return []rpc.API{ + { + Namespace: "personal", + Version: "1.0", + Service: ethapi.NewLimitedPersonalAPI(s.am), + Public: false, + }, + } +} + +// Start is run when a service is started. +// It does nothing in this case but is required by `node.Service` interface. +func (s *Service) Start(server *p2p.Server) error { + return nil +} + +// Stop is run when a service is stopped. +// It does nothing in this case but is required by `node.Service` interface. +func (s *Service) Stop() error { + return nil +} diff --git a/shhext/README.md b/services/shhext/README.md similarity index 100% rename from shhext/README.md rename to services/shhext/README.md diff --git a/shhext/api.go b/services/shhext/api.go similarity index 100% rename from shhext/api.go rename to services/shhext/api.go diff --git a/shhext/service.go b/services/shhext/service.go similarity index 100% rename from shhext/service.go rename to services/shhext/service.go diff --git a/shhext/service_test.go b/services/shhext/service_test.go similarity index 100% rename from shhext/service_test.go rename to services/shhext/service_test.go diff --git a/shhext/signal.go b/services/shhext/signal.go similarity index 100% rename from shhext/signal.go rename to services/shhext/signal.go diff --git a/sign/errors.go b/sign/errors.go index b49e31462..77992cb36 100644 --- a/sign/errors.go +++ b/sign/errors.go @@ -7,30 +7,46 @@ import ( "github.com/status-im/status-go/geth/account" ) -// TODO (mandrigin): Change values of these errors when API change is made. var ( - //ErrSignReqNotFound - error transaction hash not found - ErrSignReqNotFound = errors.New("transaction hash not found") - //ErrSignReqInProgress - error transaction is in progress - ErrSignReqInProgress = errors.New("transaction is in progress") - // TODO (mandrigin): to be moved to `transactions` package - //ErrInvalidCompleteTxSender - error transaction with invalid sender - ErrInvalidCompleteTxSender = errors.New("transaction can only be completed by the same account which created it") - //ErrSignReqTimedOut - error transaction sending timed out - ErrSignReqTimedOut = errors.New("transaction sending timed out") - //ErrSignReqDiscarded - error transaction discarded - ErrSignReqDiscarded = errors.New("transaction has been discarded") + //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 - ErrInvalidCompleteTxSender.Error(): true, // completing tx create from another account 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 } diff --git a/sign/notifications.go b/sign/notifications.go index f6214629e..d96345a58 100644 --- a/sign/notifications.go +++ b/sign/notifications.go @@ -8,23 +8,23 @@ import ( ) const ( - // EventTransactionQueued is triggered when send transaction request is queued - EventTransactionQueued = "transaction.queued" - // EventTransactionFailed is triggered when send transaction request fails - EventTransactionFailed = "transaction.failed" + // EventSignRequestAdded is triggered when send transaction request is queued + EventSignRequestAdded = "sign-request.queued" + // EventSignRequestFailed is triggered when send transaction request fails + EventSignRequestFailed = "sign-request.failed" ) const ( - // SendTransactionNoErrorCode is sent when no error occurred. - SendTransactionNoErrorCode = iota - // SendTransactionDefaultErrorCode is every case when there is no special tx return code. - SendTransactionDefaultErrorCode - // SendTransactionPasswordErrorCode is sent when account failed verification. - SendTransactionPasswordErrorCode - // SendTransactionTimeoutErrorCode is sent when tx is timed out. - SendTransactionTimeoutErrorCode - // SendTransactionDiscardedErrorCode is sent when tx was discarded. - SendTransactionDiscardedErrorCode + // 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 ) const ( @@ -48,52 +48,55 @@ func messageIDFromContext(ctx context.Context) string { } var txReturnCodes = map[error]int{ - nil: SendTransactionNoErrorCode, - keystore.ErrDecrypt: SendTransactionPasswordErrorCode, - ErrSignReqTimedOut: SendTransactionTimeoutErrorCode, - ErrSignReqDiscarded: SendTransactionDiscardedErrorCode, + nil: SignRequestNoErrorCode, + keystore.ErrDecrypt: SignRequestPasswordErrorCode, + ErrSignReqTimedOut: SignRequestTimeoutErrorCode, + ErrSignReqDiscarded: SignRequestDiscardedErrorCode, } -// SendTransactionEvent is a signal sent on a send transaction request -type SendTransactionEvent struct { +// PendingRequestEvent is a signal sent when a sign request is added +type PendingRequestEvent struct { ID string `json:"id"` + Method string `json:"method"` Args interface{} `json:"args"` MessageID string `json:"message_id"` } -// NotifyOnEnqueue returns handler that processes incoming tx queue requests +// NotifyOnEnqueue sends a signal when a sign request is added func NotifyOnEnqueue(request *Request) { signal.Send(signal.Envelope{ - Type: EventTransactionQueued, - Event: SendTransactionEvent{ + Type: EventSignRequestAdded, + Event: PendingRequestEvent{ ID: request.ID, Args: request.Meta, + Method: request.Method, MessageID: messageIDFromContext(request.context), }, }) } -// ReturnSendTransactionEvent is a JSON returned whenever transaction send is returned -type ReturnSendTransactionEvent struct { - ID string `json:"id"` - Args interface{} `json:"args"` - MessageID string `json:"message_id"` - ErrorMessage string `json:"error_message"` - ErrorCode int `json:"error_code,string"` +// PendingRequestErrorEvent is a signal sent when sign request has failed +type PendingRequestErrorEvent struct { + PendingRequestEvent + ErrorMessage string `json:"error_message"` + ErrorCode int `json:"error_code,string"` } -// NotifyOnReturn returns handler that processes responses from internal tx manager -func NotifyOnReturn(request *Request, err error) { +// NotifyIfError sends a signal only if error had happened +func NotifyIfError(request *Request, err error) { // we don't want to notify a user if tx was sent successfully if err == nil { return } signal.Send(signal.Envelope{ - Type: EventTransactionFailed, - Event: ReturnSendTransactionEvent{ - ID: request.ID, - Args: request.Meta, - MessageID: messageIDFromContext(request.context), + Type: EventSignRequestFailed, + Event: PendingRequestErrorEvent{ + PendingRequestEvent: PendingRequestEvent{ + ID: request.ID, + Args: request.Meta, + Method: request.Method, + MessageID: messageIDFromContext(request.context), + }, ErrorMessage: err.Error(), ErrorCode: sendTransactionErrorCode(err), }, @@ -104,5 +107,5 @@ func sendTransactionErrorCode(err error) int { if code, ok := txReturnCodes[err]; ok { return code } - return SendTransactionDefaultErrorCode + return SignRequestDefaultErrorCode } diff --git a/sign/pending_requests.go b/sign/pending_requests.go index 969d65a49..0f00530b1 100644 --- a/sign/pending_requests.go +++ b/sign/pending_requests.go @@ -5,7 +5,6 @@ import ( "sync" "time" - gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/geth/account" ) @@ -31,11 +30,11 @@ func NewPendingRequests() *PendingRequests { } // Add a new signing request. -func (rs *PendingRequests) Add(ctx context.Context, meta Meta, completeFunc completeFunc) (*Request, error) { +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, meta, completeFunc) + request := newRequest(ctx, method, meta, completeFunc) rs.requests[request.ID] = request rs.log.Info("signing request is created", "ID", request.ID) @@ -68,26 +67,29 @@ func (rs *PendingRequests) First() *Request { } // Approve a signing request by it's ID. Requires a valid password and a verification function. -func (rs *PendingRequests) Approve(id string, password string, verify verifyFunc) (hash gethcommon.Hash, err error) { - rs.log.Info("complete transaction", "id", id) +func (rs *PendingRequests) Approve(id string, password string, 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 hash, err + return newErrResult(err) } selectedAccount, err := verify(password) if err != nil { - rs.complete(request, hash, err) - return hash, err + rs.complete(request, EmptyResponse, err) + return newErrResult(err) } - hash, err = request.completeFunc(selectedAccount) - rs.log.Info("finally completed transaction", "id", request.ID, "hash", hash, "err", err) + response, err := request.completeFunc(selectedAccount, password) + rs.log.Info("completed sign request ", "id", request.ID, "response", response, "err", err) - rs.complete(request, hash, err) + rs.complete(request, response, err) - return hash, err + return Result{ + Response: response, + Error: err, + } } // Discard remove a signing request from the list of pending requests. @@ -97,7 +99,7 @@ func (rs *PendingRequests) Discard(id string) error { return err } - rs.complete(request, gethcommon.Hash{}, ErrSignReqDiscarded) + rs.complete(request, EmptyResponse, ErrSignReqDiscarded) return nil } @@ -105,14 +107,14 @@ func (rs *PendingRequests) Discard(id string) error { func (rs *PendingRequests) Wait(id string, timeout time.Duration) Result { request, err := rs.Get(id) if err != nil { - return Result{Error: err} + return newErrResult(err) } for { select { case rst := <-request.result: return rst case <-time.After(timeout): - rs.complete(request, gethcommon.Hash{}, ErrSignReqTimedOut) + rs.complete(request, EmptyResponse, ErrSignReqTimedOut) } } } @@ -148,13 +150,13 @@ func (rs *PendingRequests) tryLock(id string) (*Request, error) { } // complete removes the request from the list if there is no error or an error is non-transient -func (rs *PendingRequests) complete(request *Request, hash gethcommon.Hash, err error) { +func (rs *PendingRequests) complete(request *Request, response Response, err error) { rs.mu.Lock() defer rs.mu.Unlock() request.locked = false - go NotifyOnReturn(request, err) + go NotifyIfError(request, err) if err != nil && isTransient(err) { return @@ -162,10 +164,10 @@ func (rs *PendingRequests) complete(request *Request, hash gethcommon.Hash, err delete(rs.requests, request.ID) - // hash is updated only if err is nil, but transaction is not removed from a queue + // response is updated only if err is nil, but transaction is not removed from a queue result := Result{Error: err} if err == nil { - result.Hash = hash + result.Response = response } request.result <- result diff --git a/sign/pending_requests_test.go b/sign/pending_requests_test.go index 40492e352..218ebc42c 100644 --- a/sign/pending_requests_test.go +++ b/sign/pending_requests_test.go @@ -39,33 +39,35 @@ func (s *PendingRequestsSuite) SetupTest() { s.pendingRequests = NewPendingRequests() } -func (s *PendingRequestsSuite) defaultCompleteFunc() completeFunc { +func (s *PendingRequestsSuite) defaultCompleteFunc() CompleteFunc { hash := gethcommon.Hash{1} - return func(acc *account.SelectedExtKey) (gethcommon.Hash, error) { + return func(acc *account.SelectedExtKey, password string) (Response, error) { s.Nil(acc, "account should be `nil`") - return hash, nil + s.Equal(correctPassword, password) + return hash.Bytes(), nil } } -func (s *PendingRequestsSuite) delayedCompleteFunc() completeFunc { +func (s *PendingRequestsSuite) delayedCompleteFunc() CompleteFunc { hash := gethcommon.Hash{1} - return func(acc *account.SelectedExtKey) (gethcommon.Hash, error) { + return func(acc *account.SelectedExtKey, password string) (Response, error) { time.Sleep(10 * time.Millisecond) s.Nil(acc, "account should be `nil`") - return hash, nil + s.Equal(correctPassword, password) + return hash.Bytes(), nil } } -func (s *PendingRequestsSuite) errorCompleteFunc(err error) completeFunc { +func (s *PendingRequestsSuite) errorCompleteFunc(err error) CompleteFunc { hash := gethcommon.Hash{1} - return func(acc *account.SelectedExtKey) (gethcommon.Hash, error) { + return func(acc *account.SelectedExtKey, password string) (Response, error) { s.Nil(acc, "account should be `nil`") - return hash, err + return hash.Bytes(), err } } func (s *PendingRequestsSuite) TestGet() { - req, err := s.pendingRequests.Add(context.Background(), nil, s.defaultCompleteFunc()) + 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) @@ -74,16 +76,22 @@ func (s *PendingRequestsSuite) TestGet() { } } -func (s *PendingRequestsSuite) testComplete(password string, hash gethcommon.Hash, completeFunc completeFunc) (string, error) { - req, err := s.pendingRequests.Add(context.Background(), nil, completeFunc) +func (s *PendingRequestsSuite) testComplete(password string, hash gethcommon.Hash, completeFunc CompleteFunc) (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") - hash2, err := s.pendingRequests.Approve(req.ID, password, testVerifyFunc) - s.Equal(hash, hash2, "hashes should match") + result := s.pendingRequests.Approve(req.ID, password, testVerifyFunc) - return req.ID, err + 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() { @@ -119,13 +127,13 @@ func (s PendingRequestsSuite) TestMultipleComplete() { id, err := s.testComplete(correctPassword, gethcommon.Hash{1}, s.defaultCompleteFunc()) s.NoError(err, "no errors should be there") - _, err = s.pendingRequests.Approve(id, correctPassword, testVerifyFunc) + result := s.pendingRequests.Approve(id, correctPassword, testVerifyFunc) - s.Equal(ErrSignReqNotFound, err) + s.Equal(ErrSignReqNotFound, result.Error) } func (s PendingRequestsSuite) TestConcurrentComplete() { - req, err := s.pendingRequests.Add(context.Background(), nil, s.delayedCompleteFunc()) + req, err := s.pendingRequests.Add(context.Background(), "", nil, s.delayedCompleteFunc()) s.NoError(err) s.True(s.pendingRequests.Has(req.ID), "sign request should exist") @@ -135,8 +143,8 @@ func (s PendingRequestsSuite) TestConcurrentComplete() { for i := 10; i > 0; i-- { go func() { - _, err = s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) - if err == nil { + result := s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) + if result.Error == nil { approved++ } tried++ @@ -152,14 +160,14 @@ func (s PendingRequestsSuite) TestConcurrentComplete() { } func (s PendingRequestsSuite) TestWaitSuccess() { - req, err := s.pendingRequests.Add(context.Background(), nil, s.defaultCompleteFunc()) + 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() { - _, err := s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) - s.NoError(err) + result := s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) + s.NoError(result.Error) }() result := s.pendingRequests.Wait(req.ID, 1*time.Second) @@ -167,7 +175,7 @@ func (s PendingRequestsSuite) TestWaitSuccess() { } func (s PendingRequestsSuite) TestDiscard() { - req, err := s.pendingRequests.Add(context.Background(), nil, s.defaultCompleteFunc()) + req, err := s.pendingRequests.Add(context.Background(), "", nil, s.defaultCompleteFunc()) s.NoError(err) s.True(s.pendingRequests.Has(req.ID), "sign request should exist") @@ -186,14 +194,14 @@ func (s PendingRequestsSuite) TestDiscard() { func (s PendingRequestsSuite) TestWaitFail() { expectedError := errors.New("test-wait-fail") - req, err := s.pendingRequests.Add(context.Background(), nil, s.errorCompleteFunc(expectedError)) + 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() { - _, err := s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) - s.Equal(expectedError, err) + result := s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) + s.Equal(expectedError, result.Error) }() result := s.pendingRequests.Wait(req.ID, 1*time.Second) @@ -201,14 +209,14 @@ func (s PendingRequestsSuite) TestWaitFail() { } func (s PendingRequestsSuite) TestWaitTimeout() { - req, err := s.pendingRequests.Add(context.Background(), nil, s.delayedCompleteFunc()) + req, err := s.pendingRequests.Add(context.Background(), "", nil, s.delayedCompleteFunc()) s.NoError(err) s.True(s.pendingRequests.Has(req.ID), "sign request should exist") go func() { - _, err := s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) - s.NoError(err) + result := s.pendingRequests.Approve(req.ID, correctPassword, testVerifyFunc) + s.NoError(result.Error) }() result := s.pendingRequests.Wait(req.ID, 0*time.Second) diff --git a/sign/request.go b/sign/request.go index e3cfcf8a1..d8c74445d 100644 --- a/sign/request.go +++ b/sign/request.go @@ -3,12 +3,12 @@ package sign import ( "context" - "github.com/ethereum/go-ethereum/common" "github.com/pborman/uuid" "github.com/status-im/status-go/geth/account" ) -type completeFunc func(*account.SelectedExtKey) (common.Hash, error) +// CompleteFunc is a function that is called after the sign request is approved. +type CompleteFunc func(account *account.SelectedExtKey, password string) (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. @@ -17,16 +17,18 @@ 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 + completeFunc CompleteFunc result chan Result } -func newRequest(ctx context.Context, meta Meta, completeFunc completeFunc) *Request { +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, diff --git a/sign/result.go b/sign/result.go index eaa09db28..7724f2c57 100644 --- a/sign/result.go +++ b/sign/result.go @@ -1,9 +1,41 @@ package sign -import "github.com/ethereum/go-ethereum/common" +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 { - Hash common.Hash - Error error + 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, + } } diff --git a/t/e2e/api/backend_test.go b/t/e2e/api/backend_test.go index 0187a474b..f260833ac 100644 --- a/t/e2e/api/backend_test.go +++ b/t/e2e/api/backend_test.go @@ -135,26 +135,26 @@ func (s *APIBackendTestSuite) TestRaceConditions() { progress <- struct{}{} }, func(config *params.NodeConfig) { - log.Info("CompleteTransaction()") - _, err := s.Backend.CompleteTransaction("id", "password") - s.T().Logf("CompleteTransaction(), error: %v", err) + log.Info("ApproveSignRequest()") + result := s.Backend.ApproveSignRequest("id", "password") + s.T().Logf("ApproveSignRequest(), error: %v", result.Error) progress <- struct{}{} }, func(config *params.NodeConfig) { - log.Info("DiscardTransaction()") - s.T().Logf("DiscardTransaction(), error: %v", s.Backend.DiscardTransaction("id")) + log.Info("DiscardSignRequest()") + s.T().Logf("DiscardSignRequest(), error: %v", s.Backend.DiscardSignRequest("id")) progress <- struct{}{} }, func(config *params.NodeConfig) { - log.Info("CompleteTransactions()") + log.Info("ApproveSignRequests()") ids := []string{"id1", "id2"} - s.T().Logf("CompleteTransactions(), result: %v", s.Backend.CompleteTransactions(ids, "password")) + s.T().Logf("ApproveSignRequests(), result: %v", s.Backend.ApproveSignRequests(ids, "password")) progress <- struct{}{} }, func(config *params.NodeConfig) { - log.Info("DiscardTransactions()") + log.Info("DiscardSignRequests()") ids := []string{"id1", "id2"} - s.T().Logf("DiscardTransactions(), result: %v", s.Backend.DiscardTransactions(ids)) + s.T().Logf("DiscardSignRequests(), result: %v", s.Backend.DiscardSignRequests(ids)) progress <- struct{}{} }, } diff --git a/t/e2e/jail/jail_rpc_test.go b/t/e2e/jail/jail_rpc_test.go index 37b1e333a..949b23b9b 100644 --- a/t/e2e/jail/jail_rpc_test.go +++ b/t/e2e/jail/jail_rpc_test.go @@ -126,16 +126,16 @@ func (s *JailRPCTestSuite) TestContractDeployment() { unmarshalErr := json.Unmarshal([]byte(jsonEvent), &envelope) s.NoError(unmarshalErr, "cannot unmarshal JSON: %s", jsonEvent) - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) s.T().Logf("transaction queued and will be completed shortly, id: %v", event["id"]) s.NoError(s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) txID := event["id"].(string) - var txErr error - txHash, txErr = s.Backend.CompleteTransaction(txID, TestConfig.Account1.Password) - if s.NoError(txErr, event["id"]) { + result := s.Backend.ApproveSignRequest(txID, TestConfig.Account1.Password) + txHash.SetBytes(result.Response.Bytes()) + if s.NoError(result.Error, event["id"]) { s.T().Logf("contract transaction complete, URL: %s", "https://ropsten.etherscan.io/tx/"+txHash.Hex()) } @@ -284,14 +284,16 @@ func (s *JailRPCTestSuite) TestJailVMPersistence() { s.T().Errorf("cannot unmarshal event's JSON: %s", jsonEvent) return } - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) s.T().Logf("Transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string)) - //var txHash common.Hash + var txHash gethcommon.Hash txID := event["id"].(string) - txHash, e := s.Backend.CompleteTransaction(txID, TestConfig.Account1.Password) - s.NoError(e, "cannot complete queued transaction[%v]: %v", event["id"], e) + result := s.Backend.ApproveSignRequest(txID, TestConfig.Account1.Password) + s.NoError(result.Error, "cannot complete queued transaction[%v]: %v", event["id"], result.Error) + + txHash.SetBytes(result.Response.Bytes()) s.T().Logf("Transaction complete: https://ropsten.etherscan.io/tx/%s", txHash.Hex()) } diff --git a/t/e2e/services/personal_sign_test.go b/t/e2e/services/personal_sign_test.go new file mode 100644 index 000000000..08f52d6d7 --- /dev/null +++ b/t/e2e/services/personal_sign_test.go @@ -0,0 +1,221 @@ +package services + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/accounts/keystore" + acc "github.com/status-im/status-go/geth/account" + "github.com/status-im/status-go/geth/params" + "github.com/status-im/status-go/geth/signal" + "github.com/status-im/status-go/services/personal" + "github.com/status-im/status-go/sign" + e2e "github.com/status-im/status-go/t/e2e" + . "github.com/status-im/status-go/t/utils" + "github.com/stretchr/testify/suite" +) + +const ( + signDataString = "0xBAADBEEF" + accountNotExists = "0x00164ca341326a03b547c05B343b2E21eFAe2400" +) + +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) { + s := new(PersonalSignSuite) + s.upstream = true + suite.Run(t, s) +} + +type PersonalSignSuite struct { + e2e.BackendTestSuite + upstream bool +} + +func (s *PersonalSignSuite) TestPersonalSignSuccess() { + 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) notificationHandlerSuccess(account string, pass string) func(string) { + return func(jsonEvent string) { + s.notificationHandler(account, pass, nil)(jsonEvent) + } +} + +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 == sign.EventSignRequestAdded { + err := s.Backend.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password) + s.NoError(err) + } + s.notificationHandlerSuccess(account, pass)(jsonEvent) + } +} + +func (s *PersonalSignSuite) initTest(upstreamEnabled bool) error { + nodeConfig, err := MakeTestNodeConfig(GetNetworkID()) + s.NoError(err) + + nodeConfig.IPCEnabled = false + nodeConfig.HTTPHost = "" // to make sure that no HTTP interface is started + + if upstreamEnabled { + networkURL, err := GetRemoteURL() + s.NoError(err) + + nodeConfig.UpstreamConfig.Enabled = true + nodeConfig.UpstreamConfig.URL = networkURL + } + + return s.Backend.StartNode(nodeConfig) +} + +func (s *PersonalSignSuite) notificationHandler(account string, pass string, expectedError error) func(string) { + return func(jsonEvent string) { + envelope := unmarshalEnvelope(jsonEvent) + if envelope.Type == sign.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) { + // Test upstream if that's not StatusChain + if s.upstream && GetNetworkID() == params.StatusChainNetworkID { + s.T().Skip() + return + } + + if testParams.HandlerFactory == nil { + testParams.HandlerFactory = s.notificationHandlerSuccess + } + + err := s.initTest(s.upstream) + s.NoError(err) + defer func() { + err := s.Backend.StopNode() + 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( + `{"jsonrpc":"2.0","method":"personal_sign","params":["%s", "%s"],"id":67}`, + signDataString, + testParams.Account) + + result := s.Backend.CallRPC(basicCall) + if testParams.ExpectedError == nil { + s.NotContains(result, "error") + } else { + s.Contains(result, testParams.ExpectedError.Error()) + } +} + +func unmarshalEnvelope(jsonEvent string) signal.Envelope { + var envelope signal.Envelope + if e := json.Unmarshal([]byte(jsonEvent), &envelope); e != nil { + panic(e) + } + return envelope +} diff --git a/t/e2e/transactions/transactions_test.go b/t/e2e/transactions/transactions_test.go index eeae4f84a..bb2a50e6b 100644 --- a/t/e2e/transactions/transactions_test.go +++ b/t/e2e/transactions/transactions_test.go @@ -25,6 +25,10 @@ import ( 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) { suite.Run(t, new(TransactionsTestSuite)) } @@ -44,17 +48,21 @@ func (s *TransactionsTestSuite) TestCallRPCSendTransaction() { transactionCompleted := make(chan struct{}) - var txHash gethcommon.Hash + 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 == sign.EventTransactionQueued { + if sg.Type == sign.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) - txHash, err = s.Backend.CompleteTransaction(string(txID), TestConfig.Account1.Password) - s.NoError(err, "cannot complete queued transaction %s", txID) + signResult = s.Backend.ApproveSignRequest(string(txID), TestConfig.Account1.Password) + s.NoError(signResult.Error, "cannot complete queued transaction %s", txID) close(transactionCompleted) } }) @@ -77,7 +85,7 @@ func (s *TransactionsTestSuite) TestCallRPCSendTransaction() { s.FailNow("sending transaction timed out") } - s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+txHash.String()+`"}`, result) + s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+signResult.Response.Hash().Hex()+`"}`, result) } func (s *TransactionsTestSuite) TestCallRPCSendTransactionUpstream() { @@ -95,23 +103,23 @@ func (s *TransactionsTestSuite) TestCallRPCSendTransactionUpstream() { transactionCompleted := make(chan struct{}) - var txHash gethcommon.Hash + var signResult sign.Result signal.SetDefaultNodeNotificationHandler(func(rawSignal string) { var signalEnvelope signal.Envelope err := json.Unmarshal([]byte(rawSignal), &signalEnvelope) s.NoError(err) - if signalEnvelope.Type == sign.EventTransactionQueued { + if signalEnvelope.Type == sign.EventSignRequestAdded { event := signalEnvelope.Event.(map[string]interface{}) txID := event["id"].(string) // Complete with a wrong passphrase. - txHash, err = s.Backend.CompleteTransaction(string(txID), "some-invalid-passphrase") - s.EqualError(err, keystore.ErrDecrypt.Error(), "should return an error as the passphrase was invalid") + signResult = s.Backend.ApproveSignRequest(string(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. - txHash, err = s.Backend.CompleteTransaction(string(txID), TestConfig.Account2.Password) - s.NoError(err, "cannot complete queued transaction %s", txID) + signResult = s.Backend.ApproveSignRequest(string(txID), TestConfig.Account2.Password) + s.NoError(signResult.Error, "cannot complete queued transaction %s", txID) close(transactionCompleted) } @@ -135,7 +143,7 @@ func (s *TransactionsTestSuite) TestCallRPCSendTransactionUpstream() { s.FailNow("sending transaction timed out") } - s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+txHash.String()+`"}`, result) + s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+signResult.Response.Hash().Hex()+`"}`, result) } func (s *TransactionsTestSuite) TestEmptyToFieldPreserved() { @@ -154,14 +162,14 @@ func (s *TransactionsTestSuite) TestEmptyToFieldPreserved() { } err := json.Unmarshal([]byte(rawSignal), &sg) s.NoError(err) - if sg.Type == sign.EventTransactionQueued { - var event sign.SendTransactionEvent + if sg.Type == sign.EventSignRequestAdded { + var event sign.PendingRequestEvent s.NoError(json.Unmarshal(sg.Event, &event)) args := event.Args.(map[string]interface{}) s.NotNil(args["from"]) s.Nil(args["to"]) - _, err := s.Backend.CompleteTransaction(event.ID, TestConfig.Account1.Password) - s.NoError(err) + signResult := s.Backend.ApproveSignRequest(event.ID, TestConfig.Account1.Password) + s.NoError(signResult.Error) close(transactionCompleted) } }) @@ -229,6 +237,64 @@ func (s *TransactionsTestSuite) TestSendContractTx() { 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 == sign.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( + string(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( + string(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( + string(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) { s.StartTestBackend() defer s.StopTestBackend() @@ -241,60 +307,8 @@ func (s *TransactionsTestSuite) testSendContractTx(setInputAndDataValue initFunc completeQueuedTransaction := make(chan struct{}) // 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, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent)) - - if envelope.Type == sign.EventTransactionQueued { - 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") - txHash, err = s.Backend.CompleteTransaction( - string(event["id"].(string)), - TestConfig.Account1.Password, - ) - 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) - txHash, err = s.Backend.CompleteTransaction( - string(event["id"].(string)), - TestConfig.Account1.Password, - ) - s.EqualError( - err, - sign.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)) - txHash, err = s.Backend.CompleteTransaction( - string(event["id"].(string)), - TestConfig.Account1.Password, - ) - if expectedError != nil { - s.Equal(expectedError, err) - } else { - s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"])) - } - - log.Info("contract transaction complete", "URL", "https://ropsten.etherscan.io/tx/"+txHash.Hex()) - close(completeQueuedTransaction) - return - } - }) + var signRequestResult []byte + s.setDefaultNodeNotificationHandler(&signRequestResult, sampleAddress, completeQueuedTransaction, expectedError) // this call blocks, up until Complete Transaction is called byteCode, err := hexutil.Decode(`0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636ffa1caa14603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60008160020290505b9190505600a165627a7a72305820ccdadd737e4ac7039963b54cee5e5afb25fa859a275252bdcf06f653155228210029`) @@ -323,7 +337,7 @@ func (s *TransactionsTestSuite) testSendContractTx(setInputAndDataValue initFunc s.FailNow("completing transaction timed out") } - s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid") + 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") } @@ -341,53 +355,8 @@ func (s *TransactionsTestSuite) TestSendEther() { completeQueuedTransaction := make(chan struct{}) // 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, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent)) - - if envelope.Type == sign.EventTransactionQueued { - 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") - txHash, err = s.Backend.CompleteTransaction( - string(event["id"].(string)), - TestConfig.Account1.Password, - ) - 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) - txHash, err = s.Backend.CompleteTransaction( - string(event["id"].(string)), TestConfig.Account1.Password) - s.EqualError( - err, - sign.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)) - txHash, err = s.Backend.CompleteTransaction( - string(event["id"].(string)), - TestConfig.Account1.Password, - ) - s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"])) - - close(completeQueuedTransaction) - return - } - }) + 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{ @@ -403,7 +372,7 @@ func (s *TransactionsTestSuite) TestSendEther() { s.FailNow("completing transaction timed out") } - s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid") + 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") } @@ -430,17 +399,18 @@ func (s *TransactionsTestSuite) TestSendEtherTxUpstream() { err = json.Unmarshal([]byte(jsonEvent), &envelope) s.NoError(err, "cannot unmarshal JSON: %s", jsonEvent) - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string)) - txHash, err = s.Backend.CompleteTransaction( + signResult := s.Backend.ApproveSignRequest( string(event["id"].(string)), TestConfig.Account1.Password, ) - s.NoError(err, "cannot complete queued transaction[%v]", event["id"]) + s.NoError(signResult.Error, "cannot complete queued transaction[%v]", event["id"]) - log.Info("contract transaction complete", "URL", "https://ropsten.etherscan.io/tx/"+txHash.Hex()) + txHash = signResult.Response.Hash() + log.Info("contract transaction complete", "URL", txURLString(signResult)) close(completeQueuedTransaction) } }) @@ -478,33 +448,36 @@ func (s *TransactionsTestSuite) TestDoubleCompleteQueuedTransactions() { // replace transaction notification handler txFailedEventCalled := false - txHash := gethcommon.Hash{} + 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 == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) txID := string(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.CompleteTransaction(txID, TestConfig.Account1.Password+"wrong") + 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 - txHash, err = s.Backend.CompleteTransaction(txID, TestConfig.Account1.Password) - s.NoError(err) + signResult := s.Backend.ApproveSignRequest(txID, TestConfig.Account1.Password) + s.NoError(signResult.Error) + + log.Info("transaction complete", "URL", txURLString(signResult)) + + signHash = signResult.Response.Hash() - log.Info("transaction complete", "URL", "https://rinkeby.etherscan.io/tx/"+txHash.Hex()) close(completeQueuedTransaction) } - if envelope.Type == sign.EventTransactionFailed { + if envelope.Type == sign.EventSignRequestFailed { event := envelope.Event.(map[string]interface{}) log.Info("transaction return event received", "id", event["id"].(string)) @@ -519,8 +492,8 @@ func (s *TransactionsTestSuite) TestDoubleCompleteQueuedTransactions() { } }) - // this call blocks, and should return on *second* attempt to CompleteTransaction (w/ the correct password) - txHashCheck, err := s.Backend.SendTransaction(context.TODO(), transactions.SendTxArgs{ + // 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)), @@ -533,8 +506,8 @@ func (s *TransactionsTestSuite) TestDoubleCompleteQueuedTransactions() { s.FailNow("test timed out") } - s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid") - s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed") + s.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(txFailedEventCalled, "expected tx failure signal is not received") } @@ -557,7 +530,7 @@ func (s *TransactionsTestSuite) TestDiscardQueuedTransaction() { err := json.Unmarshal([]byte(jsonEvent), &envelope) s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent)) - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) txID := string(event["id"].(string)) log.Info("transaction queued (will be discarded soon)", "id", txID) @@ -565,12 +538,12 @@ func (s *TransactionsTestSuite) TestDiscardQueuedTransaction() { s.True(s.Backend.PendingSignRequests().Has(txID), "txqueue should still have test tx") // discard - err := s.Backend.DiscardTransaction(txID) + err := s.Backend.DiscardSignRequest(txID) s.NoError(err, "cannot discard tx") // try completing discarded transaction - _, err = s.Backend.CompleteTransaction(txID, TestConfig.Account1.Password) - s.EqualError(err, "transaction hash not found", "expects tx not found, but call to CompleteTransaction succeeded") + 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), @@ -579,7 +552,7 @@ func (s *TransactionsTestSuite) TestDiscardQueuedTransaction() { close(completeQueuedTransaction) } - if envelope.Type == sign.EventTransactionFailed { + if envelope.Type == sign.EventSignRequestFailed { event := envelope.Event.(map[string]interface{}) log.Info("transaction return event received", "id", event["id"].(string)) @@ -643,7 +616,7 @@ func (s *TransactionsTestSuite) TestDiscardMultipleQueuedTransactions() { var envelope signal.Envelope err := json.Unmarshal([]byte(jsonEvent), &envelope) s.NoError(err) - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) txID := string(event["id"].(string)) log.Info("transaction queued (will be discarded soon)", "id", txID) @@ -653,7 +626,7 @@ func (s *TransactionsTestSuite) TestDiscardMultipleQueuedTransactions() { txIDs <- txID } - if envelope.Type == sign.EventTransactionFailed { + if envelope.Type == sign.EventSignRequestFailed { event := envelope.Event.(map[string]interface{}) log.Info("transaction return event received", "id", event["id"].(string)) @@ -691,17 +664,17 @@ func (s *TransactionsTestSuite) TestDiscardMultipleQueuedTransactions() { txIDs = append(txIDs, "invalid-tx-id") // discard - discardResults := s.Backend.DiscardTransactions(txIDs) + discardResults := s.Backend.DiscardSignRequests(txIDs) require.Len(discardResults, 1, "cannot discard txs: %v", discardResults) - require.Error(discardResults["invalid-tx-id"], "transaction hash not found", "cannot discard txs: %v", discardResults) + require.Error(discardResults["invalid-tx-id"], sign.ErrSignReqNotFound, "cannot discard txs: %v", discardResults) // try completing discarded transaction - completeResults := s.Backend.CompleteTransactions(txIDs, TestConfig.Account1.Password) - require.Len(completeResults, testTxCount+1, "unexpected number of errors (call to CompleteTransaction should not succeed)") + 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, "transaction hash not found", "invalid error for %s", txResult.Hash.Hex()) - require.Equal(gethcommon.Hash{}, txResult.Hash, "invalid hash (expected zero): %s", txResult.Hash.Hex()) + 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 @@ -749,7 +722,7 @@ func (s *TransactionsTestSuite) TestNonExistentQueuedTransactions() { signal.SetDefaultNodeNotificationHandler(func(string) {}) // try completing non-existing transaction - _, err := s.Backend.CompleteTransaction("some-bad-transaction-id", TestConfig.Account1.Password) + 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()) } @@ -793,7 +766,7 @@ func (s *TransactionsTestSuite) sendConcurrentTransactions(testTxCount int) { err := json.Unmarshal([]byte(jsonEvent), &envelope) require.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent)) - if envelope.Type == sign.EventTransactionQueued { + if envelope.Type == sign.EventSignRequestAdded { event := envelope.Event.(map[string]interface{}) txID := string(event["id"].(string)) log.Info("transaction queued (will be completed in a single call, once aggregated)", "id", txID) @@ -816,9 +789,9 @@ func (s *TransactionsTestSuite) sendConcurrentTransactions(testTxCount int) { // wait for transactions, and complete them in a single call completeTxs := func(txIDs []string) { txIDs = append(txIDs, "invalid-tx-id") - results := s.Backend.CompleteTransactions(txIDs, TestConfig.Account1.Password) + results := s.Backend.ApproveSignRequests(txIDs, TestConfig.Account1.Password) s.Len(results, testTxCount+1) - s.EqualError(results["invalid-tx-id"].Error, "transaction hash not found") + s.EqualError(results["invalid-tx-id"].Error, sign.ErrSignReqNotFound.Error()) for txID, txResult := range results { s.False( @@ -826,10 +799,10 @@ func (s *TransactionsTestSuite) sendConcurrentTransactions(testTxCount int) { "invalid error for %s", txID, ) s.False( - txResult.Hash == (gethcommon.Hash{}) && txID != "invalid-tx-id", + len(txResult.Response.Bytes()) < 1 && txID != "invalid-tx-id", "invalid hash (expected non empty hash): %s", txID, ) - log.Info("transaction complete", "URL", "https://ropsten.etherscan.io/tx/"+txResult.Hash.Hex()) + log.Info("transaction complete", "URL", txURLString(txResult)) } time.Sleep(1 * time.Second) // make sure that tx complete signal propagates diff --git a/t/e2e/whisper/whisper_ext_test.go b/t/e2e/whisper/whisper_ext_test.go index 3884df0b5..43f668693 100644 --- a/t/e2e/whisper/whisper_ext_test.go +++ b/t/e2e/whisper/whisper_ext_test.go @@ -13,7 +13,7 @@ import ( "github.com/status-im/status-go/geth/node" "github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/geth/signal" - "github.com/status-im/status-go/shhext" + "github.com/status-im/status-go/services/shhext" "github.com/stretchr/testify/suite" ) diff --git a/vendor/github.com/ethereum/go-ethereum/ethapi/private_account.go b/vendor/github.com/ethereum/go-ethereum/ethapi/private_account.go new file mode 100644 index 000000000..8d51fd313 --- /dev/null +++ b/vendor/github.com/ethereum/go-ethereum/ethapi/private_account.go @@ -0,0 +1,26 @@ +package ethapi + +import ( + "context" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/internal/ethapi" +) + +type LimitedPersonalAPI struct { + privateAPI *ethapi.PrivateAccountAPI +} + +func NewLimitedPersonalAPI(am *accounts.Manager) *LimitedPersonalAPI { + return &LimitedPersonalAPI{ethapi.NewSubsetOfPrivateAccountAPI(am)} +} + +func (s *LimitedPersonalAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) { + return s.privateAPI.Sign(ctx, data, addr, passwd) +} + +func (s *LimitedPersonalAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) { + return s.privateAPI.EcRecover(ctx, data, sig) +} diff --git a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go index 314086335..3cee8753e 100644 --- a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go +++ b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go @@ -214,6 +214,14 @@ func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI { } } +func NewSubsetOfPrivateAccountAPI(am *accounts.Manager) *PrivateAccountAPI { + return &PrivateAccountAPI{ + am: am, + nonceLock: nil, + b: nil, + } +} + // ListAccounts will return a list of addresses for accounts this node manages. func (s *PrivateAccountAPI) ListAccounts() []common.Address { addresses := make([]common.Address, 0) // return [] instead of nil if empty @@ -426,7 +434,7 @@ func (s *PrivateAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr c // Look up the wallet containing the requested signer account := accounts.Account{Address: addr} - wallet, err := s.b.AccountManager().Find(account) + wallet, err := s.am.Find(account) if err != nil { return nil, err }