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:
Ivan Tomilov 2017-09-25 19:04:40 +03:00 committed by Ivan Daniluk
parent 750612f2bc
commit fc8f59e121
16 changed files with 501 additions and 163 deletions

View File

@ -5,7 +5,6 @@ import (
"sync" "sync"
gethcommon "github.com/ethereum/go-ethereum/common" 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/common"
"github.com/status-im/status-go/geth/jail" "github.com/status-im/status-go/geth/jail"
"github.com/status-im/status-go/geth/log" "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 // registerHandlers attaches Status callback handlers to running node
func (m *StatusBackend) registerHandlers() error { func (m *StatusBackend) registerHandlers() error {
runningNode, err := m.nodeManager.Node() rpcClient := m.NodeManager().RPCClient()
if err != nil { rpcClient.RegisterHandler("eth_accounts", m.accountManager.AccountsRPCHandler())
return err rpcClient.RegisterHandler("eth_sendTransaction", m.txQueueManager.SendTransactionRPCHandler)
}
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")
m.txQueueManager.SetTransactionQueueHandler(m.txQueueManager.TransactionQueueHandler()) m.txQueueManager.SetTransactionQueueHandler(m.txQueueManager.TransactionQueueHandler())
log.Info("Registered handler", "fn", "TransactionQueueHandler") log.Info("Registered handler", "fn", "TransactionQueueHandler")

View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/les"
"github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node" "github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/geth/params"
@ -22,14 +21,8 @@ func (s *BackendTestSuite) TestAccountsList() {
require.NoError(err) require.NoError(err)
require.NotNil(runningNode) require.NotNil(runningNode)
var lesService *les.LightEthereum accounts, err := s.backend.AccountManager().Accounts()
require.NoError(runningNode.Service(&lesService)) require.NoError(err)
require.NotNil(lesService)
accounts := lesService.StatusBackend.AccountManager().Accounts()
for _, acc := range accounts {
fmt.Println(acc.Hex())
}
// make sure that we start with empty accounts list (nobody has logged in yet) // 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)") 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) require.NoError(err)
// ensure that there is still no accounts returned // 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)") 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) // select account (sub-accounts will be created for this key)
@ -47,7 +41,8 @@ func (s *BackendTestSuite) TestAccountsList() {
require.NoError(err, "account selection failed") require.NoError(err, "account selection failed")
// at this point main account should show up // 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(1, len(accounts), "exactly single account is expected (main account)")
require.Equal(string(accounts[0].Hex()), "0x"+address, 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)) 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") require.NoError(err, "cannot create sub-account")
// now we expect to see both main account and sub-account 1 // 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(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[0].Hex()), "0x"+address, "main account is not retured as the first key")
require.Equal(string(accounts[1].Hex()), "0x"+subAccount1, "subAcount1 not returned") 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") 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) // 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(3, len(accounts), "unexpected number of accounts")
require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key") require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key")

View File

@ -388,7 +388,6 @@ func (s *BackendTestSuite) TestJailWhisper() {
throw 'message not sent: ' + JSON.stringify(message); throw 'message not sent: ' + JSON.stringify(message);
} }
var filterName = '` + whisperMessage1 + `'; var filterName = '` + whisperMessage1 + `';
var filterId = filter.filterId; var filterId = filter.filterId;
if (!filterId) { if (!filterId) {
@ -576,11 +575,13 @@ func (s *BackendTestSuite) TestJailWhisper() {
jailInstance.Parse(testCaseKey, ` jailInstance.Parse(testCaseKey, `
var shh = web3.shh; var shh = web3.shh;
// topic must be 4-byte long
var makeTopic = function () { var makeTopic = function () {
var min = 1; var topic = '0x';
var max = Math.pow(16, 8); for (var i = 0; i < 8; i++) {
var randInt = Math.floor(Math.random() * (max - min + 1)) + min; topic += Math.floor(Math.random() * 16).toString(16);
return web3.toHex(randInt); }
return topic;
}; };
`) `)

View File

@ -1,10 +1,12 @@
package api_test package api_test
import ( import (
"encoding/json"
"math/rand" "math/rand"
"testing" "testing"
"time" "time"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/les"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/api" "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 // 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. // so this test should only check StatusBackend logic with a mocked version of the underlying NodeManager.
func (s *BackendTestSuite) TestRaceConditions() { func (s *BackendTestSuite) TestRaceConditions() {

View File

@ -133,3 +133,28 @@ func (r RPCCall) ParseGasPrice() *hexutil.Big {
return (*hexutil.Big)(parsedValue) 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(),
}
}

View File

@ -125,8 +125,11 @@ type AccountManager interface {
// Logout clears whisper identities // Logout clears whisper identities
Logout() error Logout() error
// AccountsListRequestHandler returns handler to process account list request // Accounts returns handler to process account list request
AccountsListRequestHandler() func(entities []common.Address) []common.Address Accounts() ([]common.Address, error)
// AccountsRPCHandler returns RPC wrapper for Accounts()
AccountsRPCHandler() rpc.Handler
// AddressToDecryptedAccount tries to load decrypted key for a given account. // AddressToDecryptedAccount tries to load decrypted key for a given account.
// The running node, has a keystore directory which is loaded on start. Key file // 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 // TODO(adam): might be not needed
SetTransactionReturnHandler(fn EnqueuedTxReturnHandler) SetTransactionReturnHandler(fn EnqueuedTxReturnHandler)
SendTransactionRPCHandler(ctx context.Context, args ...interface{}) (interface{}, error)
// TransactionReturnHandler returns handler that processes responses from internal tx manager // TransactionReturnHandler returns handler that processes responses from internal tx manager
TransactionReturnHandler() func(queuedTx *QueuedTx, err error) TransactionReturnHandler() func(queuedTx *QueuedTx, err error)

View File

@ -348,16 +348,29 @@ func (mr *MockAccountManagerMockRecorder) Logout() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockAccountManager)(nil).Logout)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockAccountManager)(nil).Logout))
} }
// AccountsListRequestHandler mocks base method // Accounts mocks base method
func (m *MockAccountManager) AccountsListRequestHandler() func([]common.Address) []common.Address { func (m *MockAccountManager) Accounts() ([]common.Address, error) {
ret := m.ctrl.Call(m, "AccountsListRequestHandler") ret := m.ctrl.Call(m, "Accounts")
ret0, _ := ret[0].(func([]common.Address) []common.Address) 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 return ret0
} }
// AccountsListRequestHandler indicates an expected call of AccountsListRequestHandler // AccountsRPCHandler indicates an expected call of AccountsRPCHandler
func (mr *MockAccountManagerMockRecorder) AccountsListRequestHandler() *gomock.Call { func (mr *MockAccountManagerMockRecorder) AccountsRPCHandler() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsListRequestHandler", reflect.TypeOf((*MockAccountManager)(nil).AccountsListRequestHandler)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsRPCHandler", reflect.TypeOf((*MockAccountManager)(nil).AccountsRPCHandler))
} }
// AddressToDecryptedAccount mocks base method // AddressToDecryptedAccount mocks base method

View File

@ -3,7 +3,6 @@ package jail
import ( import (
"context" "context"
gethcommon "github.com/ethereum/go-ethereum/common"
gethrpc "github.com/ethereum/go-ethereum/rpc" gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/jail/internal/vm" "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. // 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) { 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() client := ep.nodeManager.RPCClient()
return ep.executeWithClient(client, vm, req) 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) { func (ep *ExecutionPolicy) executeWithClient(client *rpc.Client, vm *vm.VM, req common.RPCCall) (map[string]interface{}, error) {
// Arbitrary JSON-RPC response. // Arbitrary JSON-RPC response.
var result interface{} var result interface{}
@ -96,7 +55,9 @@ func (ep *ExecutionPolicy) executeWithClient(client *rpc.Client, vm *vm.VM, req
if client == nil { if client == nil {
resp = newErrorResponse("RPC client is not available. Node is stopped?", &req.ID) resp = newErrorResponse("RPC client is not available. Node is stopped?", &req.ID)
} else { } 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 err != nil {
if err2, ok := err.(gethrpc.Error); ok { if err2, ok := err.(gethrpc.Error); ok {
resp["error"] = map[string]interface{}{ resp["error"] = map[string]interface{}{
@ -150,32 +111,3 @@ func currentMessageID(vm *vm.VM) string {
} }
return msgID.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(),
}
}

View File

@ -2,6 +2,7 @@ package node
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -14,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/extkeys" "github.com/status-im/status-go/extkeys"
"github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/rpc"
) )
// errors // errors
@ -303,31 +305,49 @@ func (m *AccountManager) importExtendedKey(extKey *extkeys.ExtendedKey, password
return return
} }
// AccountsListRequestHandler returns handler to process account list request // Accounts returns list of addresses for selected account, including
func (m *AccountManager) AccountsListRequestHandler() func(entities []gethcommon.Address) []gethcommon.Address { // subaccounts.
return func(entities []gethcommon.Address) []gethcommon.Address { func (m *AccountManager) Accounts() ([]gethcommon.Address, error) {
if m.selectedAccount == nil { am, err := m.nodeManager.AccountManager()
return []gethcommon.Address{} 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)
} }
}
m.refreshSelectedAccount() if m.selectedAccount == nil {
return []gethcommon.Address{}, nil
}
filtered := make([]gethcommon.Address, 0) m.refreshSelectedAccount()
for _, account := range entities {
// main account filtered := make([]gethcommon.Address, 0)
if m.selectedAccount.Address.Hex() == account.Hex() { for _, account := range addresses {
filtered = append(filtered, account) // main account
} else { if m.selectedAccount.Address.Hex() == account.Hex() {
// sub accounts filtered = append(filtered, account)
for _, subAccount := range m.selectedAccount.SubAccounts { } else {
if subAccount.Address.Hex() == account.Hex() { // sub accounts
filtered = append(filtered, account) for _, subAccount := range m.selectedAccount.SubAccounts {
} if subAccount.Address.Hex() == account.Hex() {
filtered = append(filtered, account)
} }
} }
} }
}
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()
} }
} }

View File

@ -225,18 +225,42 @@ func (m *TxQueueManager) completeRemoteTransaction(queuedTx *common.QueuedTx, pa
return emptyHash, err return emptyHash, err
} }
args := queuedTx.Args
if args.GasPrice == nil {
value, err := m.gasPrice()
if err != nil {
return emptyHash, err
}
args.GasPrice = value
}
chainID := big.NewInt(int64(config.NetworkID)) chainID := big.NewInt(int64(config.NetworkID))
nonce := uint64(txCount) nonce := uint64(txCount)
gasPrice := (*big.Int)(queuedTx.Args.GasPrice) gasPrice := (*big.Int)(args.GasPrice)
dataVal := []byte(queuedTx.Args.Data) data := []byte(args.Data)
priceVal := (*big.Int)(queuedTx.Args.Value) value := (*big.Int)(args.Value)
toAddr := gethcommon.Address{}
if args.To != nil {
toAddr = *args.To
}
gas, err := m.estimateGas(queuedTx.Args) gas, err := m.estimateGas(args)
if err != nil { if err != nil {
return emptyHash, err return emptyHash, err
} }
tx := types.NewTransaction(nonce, *queuedTx.Args.To, priceVal, (*big.Int)(gas), gasPrice, dataVal) 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) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAcct.AccountKey.PrivateKey)
if err != nil { if err != nil {
return emptyHash, err return emptyHash, err
@ -305,6 +329,20 @@ func (m *TxQueueManager) estimateGas(args common.SendTxArgs) (*hexutil.Big, erro
return &estimatedGas, nil 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 // CompleteTransactions instructs backend to complete sending of multiple transactions
func (m *TxQueueManager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult { func (m *TxQueueManager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult {
results := make(map[common.QueuedTxID]common.RawCompleteTransactionResult) results := make(map[common.QueuedTxID]common.RawCompleteTransactionResult)
@ -430,3 +468,25 @@ func (m *TxQueueManager) sendTransactionErrorCode(err error) string {
func (m *TxQueueManager) SetTransactionReturnHandler(fn common.EnqueuedTxReturnHandler) { func (m *TxQueueManager) SetTransactionReturnHandler(fn common.EnqueuedTxReturnHandler) {
m.txQueue.SetTxReturnHandler(fn) 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
}

View File

@ -2,7 +2,11 @@ package rpc
import ( import (
"context" "context"
"encoding/json"
"errors"
"fmt" "fmt"
"reflect"
"sync"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/geth/params"
@ -10,6 +14,9 @@ import (
gethrpc "github.com/ethereum/go-ethereum/rpc" 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 // Client represents RPC client with custom routing
// scheme. It automatically decides where RPC call // scheme. It automatically decides where RPC call
// goes - Upstream or Local node. // goes - Upstream or Local node.
@ -21,6 +28,9 @@ type Client struct {
upstream *gethrpc.Client upstream *gethrpc.Client
router *router router *router
handlersMx sync.RWMutex // mx guards handlers
handlers map[string]Handler // locally registered handlers
} }
// NewClient initializes Client and tries to connect to both, // 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 // Client is safe for concurrent use and will automatically
// reconnect to the server if connection is lost. // reconnect to the server if connection is lost.
func NewClient(node *node.Node, upstream params.UpstreamRPCConfig) (*Client, error) { func NewClient(node *node.Node, upstream params.UpstreamRPCConfig) (*Client, error) {
c := &Client{} c := &Client{
handlers: make(map[string]Handler),
}
var err error var err error
c.local, err = node.Attach() 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. // It uses custom routing scheme for calls.
func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { 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) { if c.router.routeRemote(method) {
return c.upstream.CallContext(ctx, result, method, args...) return c.upstream.CallContext(ctx, result, method, args...)
} }
return c.local.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
}

View File

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

View File

@ -1,11 +1,13 @@
package rpc_test package rpc_test
import ( import (
"context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/geth/node" "github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/geth/rpc" "github.com/status-im/status-go/geth/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")
}

View File

@ -35,7 +35,8 @@ func (r *router) routeRemote(method string) bool {
// remoteMethods contains methods that should be routed to // remoteMethods contains methods that should be routed to
// the upstream node; the rest is considered to be routed to // the upstream node; the rest is considered to be routed to
// the local node. // 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 // 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. // 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. // 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_mining",
"eth_hashrate", "eth_hashrate",
"eth_gasPrice", "eth_gasPrice",
//"eth_accounts", // goes to the local because we handle sub-accounts //"eth_accounts", // due to sub-accounts handling
"eth_blockNumber", "eth_blockNumber",
"eth_getBalance", "eth_getBalance",
"eth_getStorageAt", "eth_getStorageAt",
@ -57,8 +58,8 @@ var remoteMethods = [...]string{
"eth_getUncleCountByBlockHash", "eth_getUncleCountByBlockHash",
"eth_getUncleCountByBlockNumber", "eth_getUncleCountByBlockNumber",
"eth_getCode", "eth_getCode",
//"eth_sign", // goes to the local because only the local node has an injected account to sign the payload with //"eth_sign", // only the local node has an injected account to sign the payload with
"eth_sendTransaction", //"eth_sendTransaction", // we handle this specially calling eth_estimateGas, signing it locally and sending eth_sendRawTransaction afterwards
"eth_sendRawTransaction", "eth_sendRawTransaction",
"eth_call", "eth_call",
"eth_estimateGas", "eth_estimateGas",

View File

@ -12,12 +12,12 @@ func TestRouteWithUpstream(t *testing.T) {
router := newRouter(true) router := newRouter(true)
for _, method := range remoteMethods { 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 { for _, method := range localMethods {
t.Run(method, func(t *testing.T) { 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) router := newRouter(false)
for _, method := range remoteMethods { 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 { 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