feat: desktop browser provider (#2465)

This commit is contained in:
Richard Ramos 2021-12-21 15:44:37 +00:00 committed by GitHub
parent 12c727df25
commit 33250c9134
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 881 additions and 15 deletions

View File

@ -84,10 +84,10 @@ func NewGethStatusBackend() *GethStatusBackend {
}
func (b *GethStatusBackend) initialize() {
statusNode := node.New()
accountManager := account.NewGethManager()
transactor := transactions.NewTransactor()
personalAPI := personal.NewAPI()
statusNode := node.New(transactor)
b.statusNode = statusNode
b.accountManager = accountManager

View File

@ -16,12 +16,16 @@ var (
// The empty slice marshals as "0x".
type HexBytes []byte
// MarshalText implements encoding.TextMarshaler
func (b HexBytes) MarshalText() ([]byte, error) {
func (b HexBytes) Bytes() []byte {
result := make([]byte, len(b)*2+2)
copy(result, `0x`)
hex.Encode(result[2:], b)
return result, nil
return result
}
// MarshalText implements encoding.TextMarshaler
func (b HexBytes) MarshalText() ([]byte, error) {
return b.Bytes(), nil
}
// UnmarshalJSON implements json.Unmarshaler.

View File

@ -453,6 +453,7 @@ func (db *Database) SaveSetting(setting string, value interface{}) error {
if err != nil {
return err
}
_, err = update.Exec(value)
return err
}
@ -647,6 +648,22 @@ func (db *Database) GetProfilePicturesVisibility() (int, error) {
return result, err
}
func (db *Database) GetPublicKey() (rst string, err error) {
err = db.db.QueryRow("SELECT public_key FROM settings WHERE synthetic_id = 'id'").Scan(&rst)
if err == sql.ErrNoRows {
return rst, nil
}
return
}
func (db *Database) GetDappsAddress() (rst types.Address, err error) {
err = db.db.QueryRow("SELECT dapps_address FROM settings WHERE synthetic_id = 'id'").Scan(&rst)
if err == sql.ErrNoRows {
return rst, nil
}
return
}
func (db *Database) CanUseMailservers() (bool, error) {
var result bool
err := db.db.QueryRow("SELECT use_mailservers FROM settings WHERE synthetic_id = 'id'").Scan(&result)

View File

@ -45,7 +45,9 @@ import (
"github.com/status-im/status-go/services/wakuext"
"github.com/status-im/status-go/services/wakuv2ext"
"github.com/status-im/status-go/services/wallet"
"github.com/status-im/status-go/services/web3provider"
"github.com/status-im/status-go/timesource"
"github.com/status-im/status-go/transactions"
"github.com/status-im/status-go/waku"
"github.com/status-im/status-go/wakuv2"
)
@ -82,6 +84,7 @@ type StatusNode struct {
gethAccountManager *account.GethManager
accountsManager *accounts.Manager
transactor *transactions.Transactor
// services
services []common.StatusService
@ -96,6 +99,7 @@ type StatusNode struct {
browsersSrvc *browsers.Service
permissionsSrvc *permissions.Service
mailserversSrvc *mailservers.Service
providerSrvc *web3provider.Service
appMetricsSrvc *appmetricsservice.Service
walletSrvc *wallet.Service
peerSrvc *peer.Service
@ -110,9 +114,10 @@ type StatusNode struct {
}
// New makes new instance of StatusNode.
func New() *StatusNode {
func New(transactor *transactions.Transactor) *StatusNode {
return &StatusNode{
gethAccountManager: account.NewGethManager(),
transactor: transactor,
log: log.New("package", "status-go/node.StatusNode"),
publicMethods: make(map[string]bool),
}
@ -403,6 +408,7 @@ func (n *StatusNode) stop() error {
n.browsersSrvc = nil
n.permissionsSrvc = nil
n.mailserversSrvc = nil
n.providerSrvc = nil
n.appMetricsSrvc = nil
n.walletSrvc = nil
n.peerSrvc = nil

View File

@ -18,7 +18,7 @@ func TestWakuLightModeEnabledSetsEmptyBloomFilter(t *testing.T) {
LightClient: true,
},
}
node := New()
node := New(nil)
require.NoError(t, node.Start(&config, &accounts.Manager{}))
defer func() {
require.NoError(t, node.Stop())
@ -41,7 +41,7 @@ func TestWakuLightModeEnabledSetsNilBloomFilter(t *testing.T) {
LightClient: false,
},
}
node := New()
node := New(nil)
require.NoError(t, node.Start(&config, &accounts.Manager{}))
defer func() {
require.NoError(t, node.Stop())

View File

@ -23,7 +23,7 @@ import (
func TestStatusNodeStart(t *testing.T) {
config, err := utils.MakeTestNodeConfigWithDataDir("", "", params.StatusChainNetworkID)
require.NoError(t, err)
n := New()
n := New(nil)
// checks before node is started
require.Nil(t, n.GethNode())
@ -74,7 +74,7 @@ func TestStatusNodeWithDataDir(t *testing.T) {
DataDir: dir,
KeyStoreDir: keyStoreDir,
}
n := New()
n := New(nil)
require.NoError(t, n.Start(&config, nil))
require.NoError(t, n.Stop())
@ -96,7 +96,7 @@ func TestStatusNodeAddPeer(t *testing.T) {
defer func() { require.NoError(t, peer.Close()) }()
peerURL := peer.Server().Self().URLv4()
n := New()
n := New(nil)
// checks before node is started
require.EqualError(t, n.AddPeer(peerURL), ErrNoRunningNode.Error())
@ -128,7 +128,7 @@ func TestStatusNodeRendezvousDiscovery(t *testing.T) {
// use custom address to test the all possibilities
AdvertiseAddr: "127.0.0.1",
}
n := New()
n := New(nil)
require.NoError(t, n.Start(&config, nil))
require.NotNil(t, n.discovery)
require.True(t, n.discovery.Running())
@ -147,7 +147,7 @@ func TestStatusNodeStartDiscoveryManual(t *testing.T) {
// use custom address to test the all possibilities
AdvertiseAddr: "127.0.0.1",
}
n := New()
n := New(nil)
require.NoError(t, n.StartWithOptions(&config, StartOptions{}))
require.Nil(t, n.discovery)
// start discovery manually
@ -162,7 +162,7 @@ func TestStatusNodeDiscoverNode(t *testing.T) {
NoDiscovery: true,
ListenAddr: "127.0.0.1:0",
}
n := New()
n := New(nil)
require.NoError(t, n.Start(&config, nil))
node, err := n.discoverNode()
require.NoError(t, err)
@ -173,7 +173,7 @@ func TestStatusNodeDiscoverNode(t *testing.T) {
AdvertiseAddr: "127.0.0.2",
ListenAddr: "127.0.0.1:0",
}
n = New()
n = New(nil)
require.NoError(t, n.Start(&config, nil))
node, err = n.discoverNode()
require.NoError(t, err)

View File

@ -17,7 +17,7 @@ func (api *TestServiceAPI) SomeMethod(_ context.Context) (string, error) {
}
func createAndStartStatusNode(config *params.NodeConfig) (*StatusNode, error) {
statusNode := New()
statusNode := New(nil)
err := statusNode.Start(config, nil)
if err != nil {
return nil, err

View File

@ -38,6 +38,7 @@ import (
"github.com/status-im/status-go/services/wakuext"
"github.com/status-im/status-go/services/wakuv2ext"
"github.com/status-im/status-go/services/wallet"
"github.com/status-im/status-go/services/web3provider"
"github.com/status-im/status-go/timesource"
"github.com/status-im/status-go/waku"
wakucommon "github.com/status-im/status-go/waku/common"
@ -69,6 +70,8 @@ func (b *StatusNode) initServices(config *params.NodeConfig) error {
services = appendIf(config.ENSConfig.Enabled, services, b.ensService())
services = appendIf(config.PermissionsConfig.Enabled, services, b.permissionsService())
services = appendIf(config.MailserversConfig.Enabled, services, b.mailserversService())
services = appendIf(config.Web3ProviderConfig.Enabled, services, b.providerService())
if config.WakuConfig.Enabled {
wakuService, err := b.wakuService(&config.WakuConfig, &config.ClusterConfig)
if err != nil {
@ -375,6 +378,13 @@ func (b *StatusNode) mailserversService() *mailservers.Service {
return b.mailserversSrvc
}
func (b *StatusNode) providerService() *web3provider.Service {
if b.providerSrvc == nil {
b.providerSrvc = web3provider.NewService(b.appDB, b.rpcClient, b.config, b.gethAccountManager, b.rpcFiltersSrvc, b.transactor)
}
return b.providerSrvc
}
func (b *StatusNode) appmetricsService() common.StatusService {
if b.appMetricsSrvc == nil {
b.appMetricsSrvc = appmetricsservice.NewService(appmetrics.NewDB(b.appDB))

View File

@ -512,6 +512,10 @@ type NodeConfig struct {
// (persistent storage of user's mailserver records).
MailserversConfig MailserversConfig
// Web3ProviderConfig extra configuration for provider.Service
// (desktop provider API)
Web3ProviderConfig Web3ProviderConfig
// SwarmConfig extra configuration for Swarm and ENS
SwarmConfig SwarmConfig `json:"SwarmConfig," validate:"structonly"`
@ -560,6 +564,11 @@ type MailserversConfig struct {
Enabled bool
}
// ProviderConfig extra configuration for provider.Service
type Web3ProviderConfig struct {
Enabled bool
}
// BridgeConfig provides configuration for Whisper-Waku bridge.
type BridgeConfig struct {
Enabled bool

View File

@ -116,7 +116,55 @@ func (db *Database) GetPermissions() (rst []DappPermissions, err error) {
return rst, nil
}
func (db *Database) GetPermissionsByDappName(dappName string) (rst *DappPermissions, err error) {
tx, err := db.db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
rst = &DappPermissions{
Name: dappName,
}
pRows, err := tx.Query("SELECT permission from permissions WHERE dapp_name = ?", dappName)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
defer pRows.Close()
var permission string
for pRows.Next() {
err = pRows.Scan(&permission)
if err != nil {
return
}
rst.Permissions = append(rst.Permissions, permission)
}
return rst, nil
}
func (db *Database) DeletePermission(name string) error {
_, err := db.db.Exec("DELETE FROM dapps WHERE name = ?", name)
return err
}
func (db *Database) DeleteDappPermission(dappName, permission string) error {
_, err := db.db.Exec("DELETE FROM permissions WHERE dapp_name = ? AND permission = ?", dappName, permission)
return err
}
func (db *Database) HasPermission(dappName string, permission string) (bool, error) {
var count uint64
err := db.db.QueryRow(`SELECT COUNT(1) FROM permissions WHERE dapp_name = ? AND permission = ?`, dappName, permission).Scan(&count)
return count > 0, err
}

View File

@ -0,0 +1,406 @@
package web3provider
import (
"encoding/json"
"errors"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
signercore "github.com/ethereum/go-ethereum/signer/core"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/transactions"
)
const Web3SendAsyncReadOnly = "web3-send-async-read-only"
const RequestAPI = "api-request"
const Web3SendAsyncCallback = "web3-send-async-callback"
const ResponseAPI = "api-response"
const Web3ResponseError = "web3-response-error"
const PermissionWeb3 = "web3"
const PermissionContactCode = "contact-code"
const PermissionUnknown = "unknown"
const ethCoinbase = "eth_coinbase"
var ErrorInvalidAPIRequest = errors.New("invalid API request")
var ErrorUnknownPermission = errors.New("unknown permission")
var authMethods = []string{
"eth_accounts",
"eth_coinbase",
"eth_sendTransaction",
"eth_sign",
"keycard_signTypedData",
"eth_signTypedData",
"eth_signTypedData_v3",
"personal_sign",
}
var signMethods = []string{
"eth_sign",
"personal_sign",
"eth_signTypedData",
"eth_signTypedData_v3",
"eth_signTypedData_v4",
}
var accMethods = []string{
"eth_accounts",
"eth_coinbase",
}
func NewAPI(s *Service) *API {
return &API{
s: s,
}
}
// API is class with methods available over RPC.
type API struct {
s *Service
}
type ETHPayload struct {
ID interface{} `json:"id,omitempty"`
JSONRPC string `json:"jsonrpc"`
From string `json:"from"`
Method string `json:"method"`
Params []interface{} `json:"params"`
Password string `json:"password,omitempty"`
}
type JSONRPCResponse struct {
ID interface{} `json:"id,omitempty"`
JSONRPC string `json:"jsonrpc"`
Result interface{} `json:"result"`
}
type Web3SendAsyncReadOnlyRequest struct {
Title string `json:"title,omitempty"`
MessageID interface{} `json:"messageId"`
Payload ETHPayload `json:"payload"`
Hostname string `json:"hostname"`
}
type Web3SendAsyncReadOnlyError struct {
Code uint `json:"code"`
Message string `json:"message,omitempty"`
}
type Web3SendAsyncReadOnlyResponse struct {
ProviderResponse
MessageID interface{} `json:"messageId"`
Error interface{} `json:"error,omitempty"`
Result interface{} `json:"result,omitempty"`
}
type APIRequest struct {
MessageID interface{} `json:"messageId,omitempty"`
Hostname string `json:"hostname"`
Permission string `json:"permission"`
}
type APIResponse struct {
ProviderResponse
MessageID interface{} `json:"messageId,omitempty"`
Permission string `json:"permission"`
Data interface{} `json:"data,omitempty"`
IsAllowed bool `json:"isAllowed"`
}
type ProviderResponse struct {
ResponseType string `json:"type"`
}
func (api *API) ProcessRequest(requestType string, payload json.RawMessage) (interface{}, error) {
switch requestType {
case RequestAPI:
var request APIRequest
if err := json.Unmarshal([]byte(payload), &request); err != nil {
return nil, err
}
return api.ProcessAPIRequest(request)
case Web3SendAsyncReadOnly:
var request Web3SendAsyncReadOnlyRequest
if err := json.Unmarshal(payload, &request); err != nil {
return nil, err
}
return api.ProcessWeb3ReadOnlyRequest(request)
default:
return nil, errors.New("invalid request type")
}
}
func contains(item string, elems []string) bool {
for _, x := range elems {
if x == item {
return true
}
}
return false
}
// web3Call returns a response from a read-only eth RPC method
func (api *API) web3Call(request Web3SendAsyncReadOnlyRequest) (*Web3SendAsyncReadOnlyResponse, error) {
var rpcResult interface{}
var errMsg interface{}
if request.Payload.Method == "personal_ecRecover" {
data, err := hexutil.Decode(request.Payload.Params[0].(string))
if err != nil {
return nil, err
}
sig, err := hexutil.Decode(request.Payload.Params[1].(string))
if err != nil {
return nil, err
}
addr, err := api.EcRecover(data, sig)
if err != nil {
return nil, err
}
rpcResult = JSONRPCResponse{
JSONRPC: "2.0",
ID: request.Payload.ID,
Result: addr.String(),
}
} else {
ethPayload, err := json.Marshal(request.Payload)
if err != nil {
return nil, err
}
response := api.s.rpcClient.CallRaw(string(ethPayload))
if response == "" {
errMsg = Web3ResponseError
}
rpcResult = json.RawMessage(response)
}
return &Web3SendAsyncReadOnlyResponse{
ProviderResponse: ProviderResponse{
ResponseType: Web3SendAsyncCallback,
},
MessageID: request.MessageID,
Error: errMsg,
Result: rpcResult,
}, nil
}
func (api *API) web3NoPermission(request Web3SendAsyncReadOnlyRequest) (*Web3SendAsyncReadOnlyResponse, error) {
return &Web3SendAsyncReadOnlyResponse{
ProviderResponse: ProviderResponse{
ResponseType: Web3SendAsyncCallback,
},
MessageID: request.MessageID,
Error: Web3SendAsyncReadOnlyError{
Code: 4100,
Message: "The requested method and/or account has not been authorized by the user.",
},
}, nil
}
func (api *API) web3AccResponse(request Web3SendAsyncReadOnlyRequest) (*Web3SendAsyncReadOnlyResponse, error) {
dappsAddress, err := api.s.accountsDB.GetDappsAddress()
if err != nil {
return nil, err
}
var result interface{}
if request.Payload.Method == ethCoinbase {
result = dappsAddress
} else {
result = []types.Address{dappsAddress}
}
return &Web3SendAsyncReadOnlyResponse{
ProviderResponse: ProviderResponse{
ResponseType: Web3SendAsyncCallback,
},
MessageID: request.MessageID,
Result: JSONRPCResponse{
JSONRPC: "2.0",
ID: request.Payload.ID,
Result: result,
},
}, nil
}
func (api *API) getVerifiedWalletAccount(address, password string) (*account.SelectedExtKey, error) {
exists, err := api.s.accountsDB.AddressExists(types.HexToAddress(address))
if err != nil {
log.Error("failed to query db for a given address", "address", address, "error", err)
return nil, err
}
if !exists {
log.Error("failed to get a selected account", "err", transactions.ErrInvalidTxSender)
return nil, transactions.ErrAccountDoesntExist
}
key, err := api.s.accountsManager.VerifyAccountPassword(api.s.config.KeyStoreDir, address, password)
if err != nil {
log.Error("failed to verify account", "account", address, "error", err)
return nil, err
}
return &account.SelectedExtKey{
Address: key.Address,
AccountKey: key,
}, nil
}
func (api *API) web3SignatureResponse(request Web3SendAsyncReadOnlyRequest) (*Web3SendAsyncReadOnlyResponse, error) {
var err error
var signature types.HexBytes
if request.Payload.Method == "eth_signTypedData" || request.Payload.Method == "eth_signTypedData_v3" {
raw := json.RawMessage(request.Payload.Params[1].(string))
var data typeddata.TypedData
err = json.Unmarshal(raw, &data)
if err == nil {
signature, err = api.signTypedData(data, request.Payload.From, request.Payload.Password)
}
} else if request.Payload.Method == "eth_signTypedData_v4" {
signature, err = api.signTypedDataV4(request.Payload.Params[1].(signercore.TypedData), request.Payload.From, request.Payload.Password)
} else {
signature, err = api.signMessage(request.Payload.Params[0], request.Payload.From, request.Payload.Password)
}
if err != nil {
log.Error("could not sign message", "err", err)
return &Web3SendAsyncReadOnlyResponse{
ProviderResponse: ProviderResponse{
ResponseType: Web3SendAsyncCallback,
},
MessageID: request.MessageID,
Error: Web3SendAsyncReadOnlyError{
Code: 4100,
Message: err.Error(),
},
}, nil
}
return &Web3SendAsyncReadOnlyResponse{
ProviderResponse: ProviderResponse{
ResponseType: Web3SendAsyncCallback,
},
MessageID: request.MessageID,
Result: JSONRPCResponse{
JSONRPC: "2.0",
ID: request.Payload.ID,
Result: signature,
},
}, nil
}
func (api *API) ProcessWeb3ReadOnlyRequest(request Web3SendAsyncReadOnlyRequest) (*Web3SendAsyncReadOnlyResponse, error) {
hasPermission, err := api.s.permissionsDB.HasPermission(request.Hostname, PermissionWeb3)
if err != nil {
return nil, err
}
if contains(request.Payload.Method, authMethods) && !hasPermission {
return api.web3NoPermission(request)
}
if contains(request.Payload.Method, accMethods) {
return api.web3AccResponse(request)
} else if contains(request.Payload.Method, signMethods) {
return api.web3SignatureResponse(request)
} else if request.Payload.Method == "eth_sendTransaction" {
jsonString, err := json.Marshal(request.Payload.Params[0])
if err != nil {
return nil, err
}
var trxArgs transactions.SendTxArgs
if err := json.Unmarshal(jsonString, &trxArgs); err != nil {
return nil, err
}
hash, err := api.sendTransaction(trxArgs, request.Payload.Password)
if err != nil {
log.Error("could not send transaction message", "err", err)
return &Web3SendAsyncReadOnlyResponse{
ProviderResponse: ProviderResponse{
ResponseType: Web3SendAsyncCallback,
},
MessageID: request.MessageID,
Error: Web3ResponseError,
}, nil
}
return &Web3SendAsyncReadOnlyResponse{
ProviderResponse: ProviderResponse{
ResponseType: Web3SendAsyncCallback,
},
MessageID: request.MessageID,
Result: JSONRPCResponse{
JSONRPC: "2.0",
ID: request.Payload.ID,
Result: hash,
},
}, nil
} else {
return api.web3Call(request)
}
}
func (api *API) ProcessAPIRequest(request APIRequest) (*APIResponse, error) {
if request.Permission == "" {
return nil, ErrorInvalidAPIRequest
}
hasPermission, err := api.s.permissionsDB.HasPermission(request.Hostname, request.Permission)
if err != nil {
return nil, err
}
if !hasPermission {
// Not allowed
return &APIResponse{
ProviderResponse: ProviderResponse{
ResponseType: ResponseAPI,
},
Permission: request.Permission,
MessageID: request.MessageID,
IsAllowed: false,
}, nil
}
var data interface{}
switch request.Permission {
case PermissionWeb3:
dappsAddress, err := api.s.accountsDB.GetDappsAddress()
if err != nil {
return nil, err
}
response := make([]interface{}, 1)
response[0] = dappsAddress
data = response
case PermissionContactCode:
pubKey, err := api.s.accountsDB.GetPublicKey()
if err != nil {
return nil, err
}
data = pubKey
default:
return nil, ErrorUnknownPermission
}
return &APIResponse{
ProviderResponse: ProviderResponse{
ResponseType: ResponseAPI,
},
Permission: request.Permission,
MessageID: request.MessageID,
Data: data,
IsAllowed: true,
}, nil
}

View File

@ -0,0 +1,200 @@
package web3provider
import (
"database/sql"
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/services/permissions"
"github.com/status-im/status-go/t/utils"
"github.com/status-im/status-go/transactions/fake"
gethrpc "github.com/ethereum/go-ethereum/rpc"
statusRPC "github.com/status-im/status-go/rpc"
)
func createDB(t *testing.T) (*sql.DB, func()) {
tmpfile, err := ioutil.TempFile("", "provider-tests-")
require.NoError(t, err)
db, err := appdatabase.InitializeDB(tmpfile.Name(), "provider-tests")
require.NoError(t, err)
return db, func() {
require.NoError(t, db.Close())
require.NoError(t, os.Remove(tmpfile.Name()))
}
}
func setupTestAPI(t *testing.T) (*API, func()) {
db, cancel := createDB(t)
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
require.NoError(t, err)
// Creating a dummy status node to simulate what it's done in get_status_node.go
upstreamConfig := params.UpstreamRPCConfig{
URL: "https://mainnet.infura.io/v3/800c641949d64d768a5070a1b0511938",
Enabled: true,
}
txServiceMockCtrl := gomock.NewController(t)
server, _ := fake.NewTestServer(txServiceMockCtrl)
client := gethrpc.DialInProc(server)
rpcClient, err := statusRPC.NewClient(client, 1, upstreamConfig, nil, db)
require.NoError(t, err)
// import account keys
utils.Init()
require.NoError(t, utils.ImportTestAccount(keyStoreDir, utils.GetAccount1PKFile()))
accManager := account.NewGethManager()
nodeConfig := &params.NodeConfig{
KeyStoreDir: keyStoreDir,
NetworkID: 1,
}
service := NewService(db, rpcClient, nodeConfig, accManager, nil, nil)
networks := json.RawMessage("{}")
settings := accounts.Settings{
DappsAddress: types.HexToAddress(utils.TestConfig.Account1.WalletAddress),
Networks: &networks,
}
accounts := []accounts.Account{
{Address: types.HexToAddress(utils.TestConfig.Account1.WalletAddress), Chat: true, Wallet: true},
}
require.NoError(t, service.accountsDB.SaveAccounts(accounts))
require.NoError(t, service.accountsDB.CreateSettings(settings, *nodeConfig))
return &API{
s: service,
}, cancel
}
func TestRequestPermission(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
request := APIRequest{
Hostname: "www.status.im",
}
_, err := api.ProcessAPIRequest(request)
require.Error(t, err)
request.Permission = PermissionWeb3
response, err := api.ProcessAPIRequest(request)
require.NoError(t, err)
require.False(t, response.IsAllowed)
require.Equal(t, ResponseAPI, response.ProviderResponse.ResponseType)
_ = api.s.permissionsDB.AddPermissions(permissions.DappPermissions{Name: "www.status.im", Permissions: []string{PermissionWeb3, PermissionContactCode, "RandomPermission"}})
response, err = api.ProcessAPIRequest(request)
require.NoError(t, err)
require.True(t, response.IsAllowed)
d := make([]interface{}, 1)
d[0] = types.HexToAddress(utils.TestConfig.Account1.WalletAddress)
var data interface{} = d
require.Equal(t, data, response.Data)
request.Permission = PermissionContactCode
response, err = api.ProcessAPIRequest(request)
require.NoError(t, err)
require.True(t, response.IsAllowed)
pubKey, _ := api.s.accountsDB.GetPublicKey()
data = pubKey
require.Equal(t, data, response.Data)
request.Permission = "RandomPermission"
_, err = api.ProcessAPIRequest(request)
require.Error(t, err)
}
func TestWeb3Call(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
request := Web3SendAsyncReadOnlyRequest{
Hostname: "www.status.im",
MessageID: 1,
Payload: ETHPayload{
ID: 1,
JSONRPC: "2.0",
From: types.HexToAddress(utils.TestConfig.Account1.WalletAddress).String(),
Method: "net_version",
Params: []interface{}{},
},
}
response, err := api.ProcessWeb3ReadOnlyRequest(request)
require.NoError(t, err)
require.Equal(t, `{"jsonrpc":"2.0","id":1,"result":"1"}`, string(response.Result.(json.RawMessage)))
request.Payload.Method = "eth_accounts"
response, err = api.ProcessWeb3ReadOnlyRequest(request)
require.NoError(t, err)
require.Equal(t, uint(4100), response.Error.(Web3SendAsyncReadOnlyError).Code)
_ = api.s.permissionsDB.AddPermissions(permissions.DappPermissions{Name: "www.status.im", Permissions: []string{PermissionWeb3}})
response, err = api.ProcessWeb3ReadOnlyRequest(request)
require.NoError(t, err)
d := make([]types.Address, 1)
d[0] = types.HexToAddress(utils.TestConfig.Account1.WalletAddress)
var data interface{} = d // eth_account is an array of addresses
require.Equal(t, data, response.Result.(JSONRPCResponse).Result)
request.Payload.Method = "eth_coinbase"
data = d[0] // eth_coinbase is an address
response, err = api.ProcessWeb3ReadOnlyRequest(request)
require.NoError(t, err)
require.Equal(t, data, response.Result.(JSONRPCResponse).Result)
}
func TestWeb3Signature(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
_ = api.s.permissionsDB.AddPermissions(permissions.DappPermissions{Name: "www.status.im", Permissions: []string{PermissionWeb3}})
request := Web3SendAsyncReadOnlyRequest{
Hostname: "www.status.im",
MessageID: 1,
Payload: ETHPayload{
ID: 1,
JSONRPC: "2.0",
From: types.HexToAddress(utils.TestConfig.Account1.WalletAddress).String(),
Method: "personal_sign",
Params: []interface{}{types.HexBytes{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
Password: "wrong-password",
},
}
response, err := api.ProcessWeb3ReadOnlyRequest(request)
require.NoError(t, err)
require.Equal(t, uint(4100), response.Error.(Web3SendAsyncReadOnlyError).Code)
require.Equal(t, "could not decrypt key with given password", response.Error.(Web3SendAsyncReadOnlyError).Message)
request.Payload.Password = utils.TestConfig.Account1.Password
response, err = api.ProcessWeb3ReadOnlyRequest(request)
require.NoError(t, err)
require.Equal(t, types.HexBytes(types.Hex2Bytes("0xc113a94f201334da86b8237c676951932d2b0ee2b539d941736da5b736f0f224448be6435846a9df9ea0085d92b107b6e49b1786e90d6604d3ef7d6f6ec19d531c")), response.Result.(JSONRPCResponse).Result.(types.HexBytes))
}

View File

@ -0,0 +1,62 @@
package web3provider
import (
"database/sql"
"github.com/ethereum/go-ethereum/p2p"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/transactions"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/permissions"
"github.com/status-im/status-go/services/rpcfilters"
)
func NewService(appDB *sql.DB, rpcClient *rpc.Client, config *params.NodeConfig, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, transactor *transactions.Transactor) *Service {
return &Service{
permissionsDB: permissions.NewDB(appDB),
accountsDB: accounts.NewDB(appDB),
rpcClient: rpcClient,
rpcFiltersSrvc: rpcFiltersSrvc,
config: config,
accountsManager: accountsManager,
transactor: transactor,
}
}
type Service struct {
permissionsDB *permissions.Database
accountsDB *accounts.Database
rpcClient *rpc.Client
rpcFiltersSrvc *rpcfilters.Service
accountsManager *account.GethManager
config *params.NodeConfig
transactor *transactions.Transactor
}
func (s *Service) Start() error {
return nil
}
func (s *Service) Stop() error {
return nil
}
func (s *Service) APIs() []gethrpc.API {
return []gethrpc.API{
{
Namespace: "provider",
Version: "0.1.0",
Service: NewAPI(s),
},
}
}
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@ -0,0 +1,104 @@
package web3provider
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common/hexutil"
signercore "github.com/ethereum/go-ethereum/signer/core"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/transactions"
)
// signMessage checks the pwd vs the selected account and signs a message
func (api *API) signMessage(data interface{}, address string, password string) (types.HexBytes, error) {
account, err := api.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
var dBytes []byte
switch d := data.(type) {
case string:
dBytes = []byte(d)
case []byte:
dBytes = d
case byte:
dBytes = []byte{d}
}
hash := crypto.TextHash(dBytes)
sig, err := crypto.Sign(hash, account.AccountKey.PrivateKey)
if err != nil {
return types.HexBytes{}, err
}
sig[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
return types.HexBytes(sig), err
}
// signTypedData accepts data and password. Gets verified account and signs typed data.
func (api *API) signTypedData(typed typeddata.TypedData, address string, password string) (types.HexBytes, error) {
account, err := api.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(api.s.config.NetworkID)
sig, err := typeddata.Sign(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}
// signTypedDataV4 accepts data and password. Gets verified account and signs typed data.
func (api *API) signTypedDataV4(typed signercore.TypedData, address string, password string) (types.HexBytes, error) {
account, err := api.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(api.s.config.NetworkID)
sig, err := typeddata.SignTypedDataV4(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}
// SendTransaction creates a new transaction and waits until it's complete.
func (api *API) sendTransaction(sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) {
verifiedAccount, err := api.getVerifiedWalletAccount(sendArgs.From.String(), password)
if err != nil {
return hash, err
}
hash, err = api.s.transactor.SendTransaction(sendArgs, verifiedAccount)
if err != nil {
return
}
go api.s.rpcFiltersSrvc.TriggerTransactionSentToUpstreamEvent(hash)
return
}
func (api *API) EcRecover(data hexutil.Bytes, sig hexutil.Bytes) (types.Address, error) {
if len(sig) != 65 {
return types.Address{}, fmt.Errorf("signature must be 65 bytes long")
}
if sig[64] != 27 && sig[64] != 28 {
return types.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
}
sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
hash := crypto.TextHash(data)
rpk, err := crypto.SigToPub(hash, sig)
if err != nil {
return types.Address{}, err
}
return crypto.PubkeyToAddress(*rpk), nil
}