Fixed eth_sendTransaction routing to the local node (#351)
* Fixed eth_sendTransaction routing to the local node * Add local RPC handlers for eth_accounts and eth_sendTransaction
This commit is contained in:
parent
750612f2bc
commit
fc8f59e121
|
@ -5,7 +5,6 @@ import (
|
|||
"sync"
|
||||
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
"github.com/status-im/status-go/geth/common"
|
||||
"github.com/status-im/status-go/geth/jail"
|
||||
"github.com/status-im/status-go/geth/log"
|
||||
|
@ -224,19 +223,9 @@ func (m *StatusBackend) DiscardTransactions(ids []common.QueuedTxID) map[common.
|
|||
|
||||
// registerHandlers attaches Status callback handlers to running node
|
||||
func (m *StatusBackend) registerHandlers() error {
|
||||
runningNode, err := m.nodeManager.Node()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var lightEthereum *les.LightEthereum
|
||||
if err := runningNode.Service(&lightEthereum); err != nil {
|
||||
log.Error("Cannot get light ethereum service", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
lightEthereum.StatusBackend.SetAccountsFilterHandler(m.accountManager.AccountsListRequestHandler())
|
||||
log.Info("Registered handler", "fn", "AccountsFilterHandler")
|
||||
rpcClient := m.NodeManager().RPCClient()
|
||||
rpcClient.RegisterHandler("eth_accounts", m.accountManager.AccountsRPCHandler())
|
||||
rpcClient.RegisterHandler("eth_sendTransaction", m.txQueueManager.SendTransactionRPCHandler)
|
||||
|
||||
m.txQueueManager.SetTransactionQueueHandler(m.txQueueManager.TransactionQueueHandler())
|
||||
log.Info("Registered handler", "fn", "TransactionQueueHandler")
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
"github.com/status-im/status-go/geth/common"
|
||||
"github.com/status-im/status-go/geth/node"
|
||||
"github.com/status-im/status-go/geth/params"
|
||||
|
@ -22,14 +21,8 @@ func (s *BackendTestSuite) TestAccountsList() {
|
|||
require.NoError(err)
|
||||
require.NotNil(runningNode)
|
||||
|
||||
var lesService *les.LightEthereum
|
||||
require.NoError(runningNode.Service(&lesService))
|
||||
require.NotNil(lesService)
|
||||
|
||||
accounts := lesService.StatusBackend.AccountManager().Accounts()
|
||||
for _, acc := range accounts {
|
||||
fmt.Println(acc.Hex())
|
||||
}
|
||||
accounts, err := s.backend.AccountManager().Accounts()
|
||||
require.NoError(err)
|
||||
|
||||
// make sure that we start with empty accounts list (nobody has logged in yet)
|
||||
require.Zero(len(accounts), "accounts returned, while there should be none (we haven't logged in yet)")
|
||||
|
@ -39,7 +32,8 @@ func (s *BackendTestSuite) TestAccountsList() {
|
|||
require.NoError(err)
|
||||
|
||||
// ensure that there is still no accounts returned
|
||||
accounts = lesService.StatusBackend.AccountManager().Accounts()
|
||||
accounts, err = s.backend.AccountManager().Accounts()
|
||||
require.NoError(err)
|
||||
require.Zero(len(accounts), "accounts returned, while there should be none (we haven't logged in yet)")
|
||||
|
||||
// select account (sub-accounts will be created for this key)
|
||||
|
@ -47,7 +41,8 @@ func (s *BackendTestSuite) TestAccountsList() {
|
|||
require.NoError(err, "account selection failed")
|
||||
|
||||
// at this point main account should show up
|
||||
accounts = lesService.StatusBackend.AccountManager().Accounts()
|
||||
accounts, err = s.backend.AccountManager().Accounts()
|
||||
require.NoError(err)
|
||||
require.Equal(1, len(accounts), "exactly single account is expected (main account)")
|
||||
require.Equal(string(accounts[0].Hex()), "0x"+address,
|
||||
fmt.Sprintf("main account is not retured as the first key: got %s, expected %s", accounts[0].Hex(), "0x"+address))
|
||||
|
@ -57,7 +52,8 @@ func (s *BackendTestSuite) TestAccountsList() {
|
|||
require.NoError(err, "cannot create sub-account")
|
||||
|
||||
// now we expect to see both main account and sub-account 1
|
||||
accounts = lesService.StatusBackend.AccountManager().Accounts()
|
||||
accounts, err = s.backend.AccountManager().Accounts()
|
||||
require.NoError(err)
|
||||
require.Equal(2, len(accounts), "exactly 2 accounts are expected (main + sub-account 1)")
|
||||
require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key")
|
||||
require.Equal(string(accounts[1].Hex()), "0x"+subAccount1, "subAcount1 not returned")
|
||||
|
@ -68,7 +64,8 @@ func (s *BackendTestSuite) TestAccountsList() {
|
|||
require.False(subAccount1 == subAccount2 || subPubKey1 == subPubKey2, "sub-account index auto-increament failed")
|
||||
|
||||
// finally, all 3 accounts should show up (main account, sub-accounts 1 and 2)
|
||||
accounts = lesService.StatusBackend.AccountManager().Accounts()
|
||||
accounts, err = s.backend.AccountManager().Accounts()
|
||||
require.NoError(err)
|
||||
require.Equal(3, len(accounts), "unexpected number of accounts")
|
||||
require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key")
|
||||
|
||||
|
|
|
@ -388,7 +388,6 @@ func (s *BackendTestSuite) TestJailWhisper() {
|
|||
throw 'message not sent: ' + JSON.stringify(message);
|
||||
}
|
||||
|
||||
|
||||
var filterName = '` + whisperMessage1 + `';
|
||||
var filterId = filter.filterId;
|
||||
if (!filterId) {
|
||||
|
@ -576,11 +575,13 @@ func (s *BackendTestSuite) TestJailWhisper() {
|
|||
|
||||
jailInstance.Parse(testCaseKey, `
|
||||
var shh = web3.shh;
|
||||
// topic must be 4-byte long
|
||||
var makeTopic = function () {
|
||||
var min = 1;
|
||||
var max = Math.pow(16, 8);
|
||||
var randInt = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
return web3.toHex(randInt);
|
||||
var topic = '0x';
|
||||
for (var i = 0; i < 8; i++) {
|
||||
topic += Math.floor(Math.random() * 16).toString(16);
|
||||
}
|
||||
return topic;
|
||||
};
|
||||
`)
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||
"github.com/status-im/status-go/geth/api"
|
||||
|
@ -256,6 +258,119 @@ func (s *BackendTestSuite) TestCallRPC() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *BackendTestSuite) TestCallRPCSendTransaction() {
|
||||
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
|
||||
s.NoError(err)
|
||||
|
||||
nodeStarted, err := s.backend.StartNode(nodeConfig)
|
||||
s.NoError(err)
|
||||
defer s.backend.StopNode()
|
||||
|
||||
<-nodeStarted
|
||||
|
||||
// Allow to sync the blockchain.
|
||||
time.Sleep(TestConfig.Node.SyncSeconds * time.Second)
|
||||
|
||||
err = s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
|
||||
s.NoError(err)
|
||||
|
||||
transactionCompleted := make(chan struct{})
|
||||
|
||||
var txHash gethcommon.Hash
|
||||
node.SetDefaultNodeNotificationHandler(func(rawSignal string) {
|
||||
var signal node.SignalEnvelope
|
||||
err := json.Unmarshal([]byte(rawSignal), &signal)
|
||||
s.NoError(err)
|
||||
|
||||
if signal.Type == node.EventTransactionQueued {
|
||||
event := signal.Event.(map[string]interface{})
|
||||
txID := event["id"].(string)
|
||||
txHash, err = s.backend.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account1.Password)
|
||||
s.NoError(err, "cannot complete queued transaction %s", txID)
|
||||
|
||||
close(transactionCompleted)
|
||||
}
|
||||
})
|
||||
|
||||
result := s.backend.CallRPC(`{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "eth_sendTransaction",
|
||||
"params": [{
|
||||
"from": "` + TestConfig.Account1.Address + `",
|
||||
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
|
||||
"value": "0x9184e72a"
|
||||
}]
|
||||
}`)
|
||||
s.NotContains(result, "error")
|
||||
|
||||
select {
|
||||
case <-transactionCompleted:
|
||||
case <-time.After(time.Minute):
|
||||
s.FailNow("sending transaction timed out")
|
||||
}
|
||||
|
||||
s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+txHash.String()+`"}`, result)
|
||||
}
|
||||
|
||||
func (s *BackendTestSuite) TestCallRPCSendTransactionUpstream() {
|
||||
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
|
||||
s.NoError(err)
|
||||
|
||||
nodeConfig.UpstreamConfig.Enabled = true
|
||||
nodeConfig.UpstreamConfig.URL = "https://ropsten.infura.io/nKmXgiFgc2KqtoQ8BCGJ"
|
||||
|
||||
nodeStarted, err := s.backend.StartNode(nodeConfig)
|
||||
s.NoError(err)
|
||||
defer s.backend.StopNode()
|
||||
|
||||
<-nodeStarted
|
||||
|
||||
// Allow to sync the blockchain.
|
||||
time.Sleep(TestConfig.Node.SyncSeconds * time.Second)
|
||||
|
||||
err = s.backend.AccountManager().SelectAccount(TestConfig.Account2.Address, TestConfig.Account2.Password)
|
||||
s.NoError(err)
|
||||
|
||||
transactionCompleted := make(chan struct{})
|
||||
|
||||
var txHash gethcommon.Hash
|
||||
node.SetDefaultNodeNotificationHandler(func(rawSignal string) {
|
||||
var signal node.SignalEnvelope
|
||||
err := json.Unmarshal([]byte(rawSignal), &signal)
|
||||
s.NoError(err)
|
||||
|
||||
if signal.Type == node.EventTransactionQueued {
|
||||
event := signal.Event.(map[string]interface{})
|
||||
txID := event["id"].(string)
|
||||
txHash, err = s.backend.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account2.Password)
|
||||
s.NoError(err, "cannot complete queued transaction %s", txID)
|
||||
|
||||
close(transactionCompleted)
|
||||
}
|
||||
})
|
||||
|
||||
result := s.backend.CallRPC(`{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "eth_sendTransaction",
|
||||
"params": [{
|
||||
"from": "` + TestConfig.Account2.Address + `",
|
||||
"to": "` + TestConfig.Account1.Address + `",
|
||||
"value": "0x9184e72a"
|
||||
}]
|
||||
}`)
|
||||
s.NotContains(result, "error")
|
||||
|
||||
select {
|
||||
case <-transactionCompleted:
|
||||
case <-time.After(time.Minute):
|
||||
s.FailNow("sending transaction timed out")
|
||||
}
|
||||
|
||||
s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+txHash.String()+`"}`, result)
|
||||
}
|
||||
|
||||
// FIXME(tiabc): There's also a test with the same name in geth/node/manager_test.go
|
||||
// so this test should only check StatusBackend logic with a mocked version of the underlying NodeManager.
|
||||
func (s *BackendTestSuite) TestRaceConditions() {
|
||||
|
|
|
@ -133,3 +133,28 @@ func (r RPCCall) ParseGasPrice() *hexutil.Big {
|
|||
|
||||
return (*hexutil.Big)(parsedValue)
|
||||
}
|
||||
|
||||
// ToSendTxArgs converts RPCCall to SendTxArgs.
|
||||
func (r RPCCall) ToSendTxArgs() SendTxArgs {
|
||||
var err error
|
||||
var fromAddr, toAddr gethcommon.Address
|
||||
|
||||
fromAddr, err = r.ParseFromAddress()
|
||||
if err != nil {
|
||||
fromAddr = gethcommon.HexToAddress("0x0")
|
||||
}
|
||||
|
||||
toAddr, err = r.ParseToAddress()
|
||||
if err != nil {
|
||||
toAddr = gethcommon.HexToAddress("0x0")
|
||||
}
|
||||
|
||||
return SendTxArgs{
|
||||
To: &toAddr,
|
||||
From: fromAddr,
|
||||
Value: r.ParseValue(),
|
||||
Data: r.ParseData(),
|
||||
Gas: r.ParseGas(),
|
||||
GasPrice: r.ParseGasPrice(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,8 +125,11 @@ type AccountManager interface {
|
|||
// Logout clears whisper identities
|
||||
Logout() error
|
||||
|
||||
// AccountsListRequestHandler returns handler to process account list request
|
||||
AccountsListRequestHandler() func(entities []common.Address) []common.Address
|
||||
// Accounts returns handler to process account list request
|
||||
Accounts() ([]common.Address, error)
|
||||
|
||||
// AccountsRPCHandler returns RPC wrapper for Accounts()
|
||||
AccountsRPCHandler() rpc.Handler
|
||||
|
||||
// AddressToDecryptedAccount tries to load decrypted key for a given account.
|
||||
// The running node, has a keystore directory which is loaded on start. Key file
|
||||
|
@ -224,6 +227,8 @@ type TxQueueManager interface {
|
|||
// TODO(adam): might be not needed
|
||||
SetTransactionReturnHandler(fn EnqueuedTxReturnHandler)
|
||||
|
||||
SendTransactionRPCHandler(ctx context.Context, args ...interface{}) (interface{}, error)
|
||||
|
||||
// TransactionReturnHandler returns handler that processes responses from internal tx manager
|
||||
TransactionReturnHandler() func(queuedTx *QueuedTx, err error)
|
||||
|
||||
|
|
|
@ -348,16 +348,29 @@ func (mr *MockAccountManagerMockRecorder) Logout() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockAccountManager)(nil).Logout))
|
||||
}
|
||||
|
||||
// AccountsListRequestHandler mocks base method
|
||||
func (m *MockAccountManager) AccountsListRequestHandler() func([]common.Address) []common.Address {
|
||||
ret := m.ctrl.Call(m, "AccountsListRequestHandler")
|
||||
ret0, _ := ret[0].(func([]common.Address) []common.Address)
|
||||
// Accounts mocks base method
|
||||
func (m *MockAccountManager) Accounts() ([]common.Address, error) {
|
||||
ret := m.ctrl.Call(m, "Accounts")
|
||||
ret0, _ := ret[0].([]common.Address)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Accounts indicates an expected call of Accounts
|
||||
func (mr *MockAccountManagerMockRecorder) Accounts() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accounts", reflect.TypeOf((*MockAccountManager)(nil).Accounts))
|
||||
}
|
||||
|
||||
// AccountsRPCHandler mocks base method
|
||||
func (m *MockAccountManager) AccountsRPCHandler() rpc.Handler {
|
||||
ret := m.ctrl.Call(m, "AccountsRPCHandler")
|
||||
ret0, _ := ret[0].(rpc.Handler)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AccountsListRequestHandler indicates an expected call of AccountsListRequestHandler
|
||||
func (mr *MockAccountManagerMockRecorder) AccountsListRequestHandler() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsListRequestHandler", reflect.TypeOf((*MockAccountManager)(nil).AccountsListRequestHandler))
|
||||
// AccountsRPCHandler indicates an expected call of AccountsRPCHandler
|
||||
func (mr *MockAccountManagerMockRecorder) AccountsRPCHandler() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsRPCHandler", reflect.TypeOf((*MockAccountManager)(nil).AccountsRPCHandler))
|
||||
}
|
||||
|
||||
// AddressToDecryptedAccount mocks base method
|
||||
|
|
|
@ -3,7 +3,6 @@ package jail
|
|||
import (
|
||||
"context"
|
||||
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
gethrpc "github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/status-im/status-go/geth/common"
|
||||
"github.com/status-im/status-go/geth/jail/internal/vm"
|
||||
|
@ -32,50 +31,10 @@ func NewExecutionPolicy(
|
|||
|
||||
// Execute handles the execution of a RPC request and routes appropriately to either a local or remote ethereum node.
|
||||
func (ep *ExecutionPolicy) Execute(req common.RPCCall, vm *vm.VM) (map[string]interface{}, error) {
|
||||
if params.SendTransactionMethodName == req.Method {
|
||||
return ep.executeSendTransaction(vm, req)
|
||||
}
|
||||
|
||||
client := ep.nodeManager.RPCClient()
|
||||
|
||||
return ep.executeWithClient(client, vm, req)
|
||||
}
|
||||
|
||||
// executeRemoteSendTransaction defines a function to execute RPC method eth_sendTransaction over the upstream server.
|
||||
func (ep *ExecutionPolicy) executeSendTransaction(vm *vm.VM, req common.RPCCall) (map[string]interface{}, error) {
|
||||
messageID, err := preProcessRequest(vm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(adam): check if context is used
|
||||
ctx := context.WithValue(context.Background(), common.MessageIDKey, messageID)
|
||||
args := sendTxArgsFromRPCCall(req)
|
||||
|
||||
tx := ep.txQueueManager.CreateTransaction(ctx, args)
|
||||
|
||||
if err := ep.txQueueManager.QueueTransaction(tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ep.txQueueManager.WaitForTransaction(tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// invoke post processing
|
||||
postProcessRequest(vm, req, messageID)
|
||||
|
||||
res := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": req.ID,
|
||||
// @TODO(adam): which one is actually used?
|
||||
"result": tx.Hash.Hex(),
|
||||
"hash": tx.Hash.Hex(),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (ep *ExecutionPolicy) executeWithClient(client *rpc.Client, vm *vm.VM, req common.RPCCall) (map[string]interface{}, error) {
|
||||
// Arbitrary JSON-RPC response.
|
||||
var result interface{}
|
||||
|
@ -96,7 +55,9 @@ func (ep *ExecutionPolicy) executeWithClient(client *rpc.Client, vm *vm.VM, req
|
|||
if client == nil {
|
||||
resp = newErrorResponse("RPC client is not available. Node is stopped?", &req.ID)
|
||||
} else {
|
||||
err = client.Call(&result, req.Method, req.Params...)
|
||||
// TODO(adam): check if context is used
|
||||
ctx := context.WithValue(context.Background(), common.MessageIDKey, messageID)
|
||||
err = client.CallContext(ctx, &result, req.Method, req.Params...)
|
||||
if err != nil {
|
||||
if err2, ok := err.(gethrpc.Error); ok {
|
||||
resp["error"] = map[string]interface{}{
|
||||
|
@ -150,32 +111,3 @@ func currentMessageID(vm *vm.VM) string {
|
|||
}
|
||||
return msgID.String()
|
||||
}
|
||||
|
||||
func sendTxArgsFromRPCCall(req common.RPCCall) common.SendTxArgs {
|
||||
// no need to persist extra state for other requests
|
||||
if req.Method != params.SendTransactionMethodName {
|
||||
return common.SendTxArgs{}
|
||||
}
|
||||
|
||||
var err error
|
||||
var fromAddr, toAddr gethcommon.Address
|
||||
|
||||
fromAddr, err = req.ParseFromAddress()
|
||||
if err != nil {
|
||||
fromAddr = gethcommon.HexToAddress("0x0")
|
||||
}
|
||||
|
||||
toAddr, err = req.ParseToAddress()
|
||||
if err != nil {
|
||||
toAddr = gethcommon.HexToAddress("0x0")
|
||||
}
|
||||
|
||||
return common.SendTxArgs{
|
||||
To: &toAddr,
|
||||
From: fromAddr,
|
||||
Value: req.ParseValue(),
|
||||
Data: req.ParseData(),
|
||||
Gas: req.ParseGas(),
|
||||
GasPrice: req.ParseGasPrice(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package node
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
"github.com/status-im/status-go/geth/common"
|
||||
"github.com/status-im/status-go/geth/rpc"
|
||||
)
|
||||
|
||||
// errors
|
||||
|
@ -303,17 +305,29 @@ func (m *AccountManager) importExtendedKey(extKey *extkeys.ExtendedKey, password
|
|||
return
|
||||
}
|
||||
|
||||
// AccountsListRequestHandler returns handler to process account list request
|
||||
func (m *AccountManager) AccountsListRequestHandler() func(entities []gethcommon.Address) []gethcommon.Address {
|
||||
return func(entities []gethcommon.Address) []gethcommon.Address {
|
||||
// Accounts returns list of addresses for selected account, including
|
||||
// subaccounts.
|
||||
func (m *AccountManager) Accounts() ([]gethcommon.Address, error) {
|
||||
am, err := m.nodeManager.AccountManager()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var addresses []gethcommon.Address
|
||||
for _, wallet := range am.Wallets() {
|
||||
for _, account := range wallet.Accounts() {
|
||||
addresses = append(addresses, account.Address)
|
||||
}
|
||||
}
|
||||
|
||||
if m.selectedAccount == nil {
|
||||
return []gethcommon.Address{}
|
||||
return []gethcommon.Address{}, nil
|
||||
}
|
||||
|
||||
m.refreshSelectedAccount()
|
||||
|
||||
filtered := make([]gethcommon.Address, 0)
|
||||
for _, account := range entities {
|
||||
for _, account := range addresses {
|
||||
// main account
|
||||
if m.selectedAccount.Address.Hex() == account.Hex() {
|
||||
filtered = append(filtered, account)
|
||||
|
@ -327,7 +341,13 @@ func (m *AccountManager) AccountsListRequestHandler() func(entities []gethcommon
|
|||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// AccountsRPCHandler returns RPC Handler for the Accounts() method.
|
||||
func (m *AccountManager) AccountsRPCHandler() rpc.Handler {
|
||||
return func(context.Context, ...interface{}) (interface{}, error) {
|
||||
return m.Accounts()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -225,18 +225,42 @@ func (m *TxQueueManager) completeRemoteTransaction(queuedTx *common.QueuedTx, pa
|
|||
return emptyHash, err
|
||||
}
|
||||
|
||||
chainID := big.NewInt(int64(config.NetworkID))
|
||||
nonce := uint64(txCount)
|
||||
gasPrice := (*big.Int)(queuedTx.Args.GasPrice)
|
||||
dataVal := []byte(queuedTx.Args.Data)
|
||||
priceVal := (*big.Int)(queuedTx.Args.Value)
|
||||
args := queuedTx.Args
|
||||
|
||||
gas, err := m.estimateGas(queuedTx.Args)
|
||||
if args.GasPrice == nil {
|
||||
value, err := m.gasPrice()
|
||||
if err != nil {
|
||||
return emptyHash, err
|
||||
}
|
||||
|
||||
tx := types.NewTransaction(nonce, *queuedTx.Args.To, priceVal, (*big.Int)(gas), gasPrice, dataVal)
|
||||
args.GasPrice = value
|
||||
}
|
||||
|
||||
chainID := big.NewInt(int64(config.NetworkID))
|
||||
nonce := uint64(txCount)
|
||||
gasPrice := (*big.Int)(args.GasPrice)
|
||||
data := []byte(args.Data)
|
||||
value := (*big.Int)(args.Value)
|
||||
toAddr := gethcommon.Address{}
|
||||
if args.To != nil {
|
||||
toAddr = *args.To
|
||||
}
|
||||
|
||||
gas, err := m.estimateGas(args)
|
||||
if err != nil {
|
||||
return emptyHash, err
|
||||
}
|
||||
|
||||
log.Info(
|
||||
"preparing raw transaction",
|
||||
"from", args.From.Hex(),
|
||||
"to", toAddr.Hex(),
|
||||
"gas", gas,
|
||||
"gasPrice", gasPrice,
|
||||
"value", value,
|
||||
)
|
||||
|
||||
tx := types.NewTransaction(nonce, toAddr, value, (*big.Int)(gas), gasPrice, data)
|
||||
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAcct.AccountKey.PrivateKey)
|
||||
if err != nil {
|
||||
return emptyHash, err
|
||||
|
@ -305,6 +329,20 @@ func (m *TxQueueManager) estimateGas(args common.SendTxArgs) (*hexutil.Big, erro
|
|||
return &estimatedGas, nil
|
||||
}
|
||||
|
||||
func (m *TxQueueManager) gasPrice() (*hexutil.Big, error) {
|
||||
client := m.nodeManager.RPCClient()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
var gasPrice hexutil.Big
|
||||
if err := client.CallContext(ctx, &gasPrice, "eth_gasPrice"); err != nil {
|
||||
log.Warn("failed to get gas price", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gasPrice, nil
|
||||
}
|
||||
|
||||
// CompleteTransactions instructs backend to complete sending of multiple transactions
|
||||
func (m *TxQueueManager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult {
|
||||
results := make(map[common.QueuedTxID]common.RawCompleteTransactionResult)
|
||||
|
@ -430,3 +468,25 @@ func (m *TxQueueManager) sendTransactionErrorCode(err error) string {
|
|||
func (m *TxQueueManager) SetTransactionReturnHandler(fn common.EnqueuedTxReturnHandler) {
|
||||
m.txQueue.SetTxReturnHandler(fn)
|
||||
}
|
||||
|
||||
// SendTransactionRPCHandler is a handler for eth_sendTransaction method.
|
||||
// It accepts one param which is a slice with a map of transaction params.
|
||||
func (m *TxQueueManager) SendTransactionRPCHandler(ctx context.Context, args ...interface{}) (interface{}, error) {
|
||||
log.Info("SendTransactionRPCHandler called")
|
||||
|
||||
// TODO(adam): it's a hack to parse arguments as common.RPCCall can do that.
|
||||
// We should refactor parsing these params to a separate struct.
|
||||
rpcCall := common.RPCCall{Params: args}
|
||||
|
||||
tx := m.CreateTransaction(ctx, rpcCall.ToSendTxArgs())
|
||||
|
||||
if err := m.QueueTransaction(tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := m.WaitForTransaction(tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx.Hash.Hex(), nil
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/status-im/status-go/geth/params"
|
||||
|
@ -10,6 +14,9 @@ import (
|
|||
gethrpc "github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
// Handler defines handler for RPC methods.
|
||||
type Handler func(context.Context, ...interface{}) (interface{}, error)
|
||||
|
||||
// Client represents RPC client with custom routing
|
||||
// scheme. It automatically decides where RPC call
|
||||
// goes - Upstream or Local node.
|
||||
|
@ -21,6 +28,9 @@ type Client struct {
|
|||
upstream *gethrpc.Client
|
||||
|
||||
router *router
|
||||
|
||||
handlersMx sync.RWMutex // mx guards handlers
|
||||
handlers map[string]Handler // locally registered handlers
|
||||
}
|
||||
|
||||
// NewClient initializes Client and tries to connect to both,
|
||||
|
@ -29,7 +39,9 @@ type Client struct {
|
|||
// Client is safe for concurrent use and will automatically
|
||||
// reconnect to the server if connection is lost.
|
||||
func NewClient(node *node.Node, upstream params.UpstreamRPCConfig) (*Client, error) {
|
||||
c := &Client{}
|
||||
c := &Client{
|
||||
handlers: make(map[string]Handler),
|
||||
}
|
||||
|
||||
var err error
|
||||
c.local, err = node.Attach()
|
||||
|
@ -72,8 +84,92 @@ func (c *Client) Call(result interface{}, method string, args ...interface{}) er
|
|||
//
|
||||
// It uses custom routing scheme for calls.
|
||||
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...)
|
||||
}
|
||||
|
||||
if c.router.routeRemote(method) {
|
||||
return c.upstream.CallContext(ctx, result, method, args...)
|
||||
}
|
||||
return c.local.CallContext(ctx, result, method, args...)
|
||||
}
|
||||
|
||||
// RegisterHandler registers local handler for specific RPC method.
|
||||
//
|
||||
// If method is registered, it will be executed with given handler and
|
||||
// never routed to the upstream or local servers.
|
||||
func (c *Client) RegisterHandler(method string, handler Handler) {
|
||||
c.handlersMx.Lock()
|
||||
defer c.handlersMx.Unlock()
|
||||
|
||||
c.handlers[method] = handler
|
||||
}
|
||||
|
||||
// callMethod calls registered RPC handler with given args and pointer to result.
|
||||
// It handles proper params and result converting
|
||||
//
|
||||
// TODO(divan): use cancellation via context here?
|
||||
func (c *Client) callMethod(ctx context.Context, result interface{}, handler Handler, args ...interface{}) error {
|
||||
response, err := handler(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if result is nil, just ignore result -
|
||||
// the same way as gethrpc.CallContext() caller would expect
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := setResultFromRPCResponse(result, response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handler is a concurrently safe method to get registered handler by name.
|
||||
func (c *Client) handler(method string) (Handler, bool) {
|
||||
c.handlersMx.RLock()
|
||||
defer c.handlersMx.RUnlock()
|
||||
handler, ok := c.handlers[method]
|
||||
return handler, ok
|
||||
}
|
||||
|
||||
// setResultFromRPCResponse tries to set result value from response using reflection
|
||||
// as concrete types are unknown.
|
||||
func setResultFromRPCResponse(result, response interface{}) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("invalid result type: %s", r)
|
||||
}
|
||||
}()
|
||||
|
||||
responseValue := reflect.ValueOf(response)
|
||||
|
||||
// If it is called via CallRaw, result has type json.RawMessage and
|
||||
// we should marshal the response before setting it.
|
||||
// Otherwise, it is called with CallContext and result is of concrete type,
|
||||
// thus we should try to set it as it is.
|
||||
// If response type and result type are incorrect, an error should be returned.
|
||||
// TODO(divan): add additional checks for result underlying value, if needed:
|
||||
// some example: https://golang.org/src/encoding/json/decode.go#L596
|
||||
switch reflect.ValueOf(result).Elem().Type() {
|
||||
case reflect.TypeOf(json.RawMessage{}), reflect.TypeOf([]byte{}):
|
||||
data, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseValue = reflect.ValueOf(data)
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(result).Elem()
|
||||
if !value.CanSet() {
|
||||
return errors.New("can't assign value to result")
|
||||
}
|
||||
value.Set(responseValue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetResultFromRPCResponse(t *testing.T) {
|
||||
var err error
|
||||
|
||||
var resultRawMessage json.RawMessage
|
||||
err = setResultFromRPCResponse(&resultRawMessage, []string{"one", "two", "three"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, json.RawMessage(`["one","two","three"]`), resultRawMessage)
|
||||
|
||||
var resultSlice []int
|
||||
err = setResultFromRPCResponse(&resultSlice, []int{1, 2, 3})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []int{1, 2, 3}, resultSlice)
|
||||
|
||||
var resultMap map[string]interface{}
|
||||
err = setResultFromRPCResponse(&resultMap, map[string]interface{}{"test": true})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]interface{}{"test": true}, resultMap)
|
||||
|
||||
var resultStruct struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
err = setResultFromRPCResponse(&resultStruct, struct {
|
||||
A int
|
||||
B string
|
||||
}{5, "test"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, struct {
|
||||
A int
|
||||
B string
|
||||
}{5, "test"}, resultStruct)
|
||||
|
||||
var resultIncorrectType []int
|
||||
err = setResultFromRPCResponse(&resultIncorrectType, []string{"a", "b"})
|
||||
require.Error(t, err)
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
package rpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/status-im/status-go/geth/node"
|
||||
"github.com/status-im/status-go/geth/params"
|
||||
"github.com/status-im/status-go/geth/rpc"
|
||||
|
@ -242,3 +244,40 @@ func (s *RPCTestSuite) TestCallRPC() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCallRawResult checks if returned response is a valid JSON-RPC response.
|
||||
func (s *RPCTestSuite) TestCallRawResult() {
|
||||
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
|
||||
s.NoError(err)
|
||||
|
||||
nodeStarted, err := s.NodeManager.StartNode(nodeConfig)
|
||||
s.NoError(err)
|
||||
defer s.NodeManager.StopNode()
|
||||
|
||||
<-nodeStarted
|
||||
|
||||
client := s.NodeManager.RPCClient()
|
||||
|
||||
jsonResult := client.CallRaw(`{"jsonrpc":"2.0","method":"shh_version","params":[],"id":67}`)
|
||||
s.Equal(`{"jsonrpc":"2.0","id":67,"result":"5.0"}`, jsonResult)
|
||||
}
|
||||
|
||||
// TestCallContextResult checks if result passed to CallContext
|
||||
// is set accordingly to its underlying memory layout.
|
||||
func (s *RPCTestSuite) TestCallContextResult() {
|
||||
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
|
||||
s.NoError(err)
|
||||
|
||||
nodeStarted, err := s.NodeManager.StartNode(nodeConfig)
|
||||
s.NoError(err)
|
||||
defer s.NodeManager.StopNode()
|
||||
|
||||
<-nodeStarted
|
||||
|
||||
client := s.NodeManager.RPCClient()
|
||||
|
||||
var blockNumber hexutil.Uint
|
||||
err = client.CallContext(context.Background(), &blockNumber, "eth_blockNumber")
|
||||
s.NoError(err)
|
||||
s.True(blockNumber > 0, "blockNumber should be higher than 0")
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ func (r *router) routeRemote(method string) bool {
|
|||
// remoteMethods contains methods that should be routed to
|
||||
// the upstream node; the rest is considered to be routed to
|
||||
// the local node.
|
||||
// TODO(tiabc): Write a test on each of these methods to ensure they're all routed to the proper node and ensure they really work.
|
||||
// TODO(tiabc): Write a test on each of these methods to ensure they're all routed to the proper node and ensure they really work
|
||||
// TODO(tiabc: as we already caught https://github.com/status-im/status-go/issues/350 as the result of missing such test.
|
||||
// Although it's tempting to only list methods coming to the local node as there're fewer of them
|
||||
// but it's deceptive: we want to ensure that only known requests leave our zone of responsibility.
|
||||
// Also, we want new requests in newer Geth versions not to be accidentally routed to the upstream.
|
||||
|
@ -47,7 +48,7 @@ var remoteMethods = [...]string{
|
|||
"eth_mining",
|
||||
"eth_hashrate",
|
||||
"eth_gasPrice",
|
||||
//"eth_accounts", // goes to the local because we handle sub-accounts
|
||||
//"eth_accounts", // due to sub-accounts handling
|
||||
"eth_blockNumber",
|
||||
"eth_getBalance",
|
||||
"eth_getStorageAt",
|
||||
|
@ -57,8 +58,8 @@ var remoteMethods = [...]string{
|
|||
"eth_getUncleCountByBlockHash",
|
||||
"eth_getUncleCountByBlockNumber",
|
||||
"eth_getCode",
|
||||
//"eth_sign", // goes to the local because only the local node has an injected account to sign the payload with
|
||||
"eth_sendTransaction",
|
||||
//"eth_sign", // only the local node has an injected account to sign the payload with
|
||||
//"eth_sendTransaction", // we handle this specially calling eth_estimateGas, signing it locally and sending eth_sendRawTransaction afterwards
|
||||
"eth_sendRawTransaction",
|
||||
"eth_call",
|
||||
"eth_estimateGas",
|
||||
|
|
|
@ -12,12 +12,12 @@ func TestRouteWithUpstream(t *testing.T) {
|
|||
router := newRouter(true)
|
||||
|
||||
for _, method := range remoteMethods {
|
||||
require.True(t, router.routeRemote(method))
|
||||
require.True(t, router.routeRemote(method), "method "+method+" should routed to remote")
|
||||
}
|
||||
|
||||
for _, method := range localMethods {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
require.False(t, router.routeRemote(method))
|
||||
require.False(t, router.routeRemote(method), "method "+method+" should routed to local")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -26,10 +26,10 @@ func TestRouteWithoutUpstream(t *testing.T) {
|
|||
router := newRouter(false)
|
||||
|
||||
for _, method := range remoteMethods {
|
||||
require.True(t, router.routeRemote(method))
|
||||
require.False(t, router.routeRemote(method), "method "+method+" should routed to locally without UpstreamEnabled")
|
||||
}
|
||||
|
||||
for _, method := range localMethods {
|
||||
require.True(t, router.routeRemote(method))
|
||||
require.False(t, router.routeRemote(method), "method "+method+" should routed to local")
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue