feat: desktop browser provider (#2465)
This commit is contained in:
parent
12c727df25
commit
33250c9134
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 := ¶ms.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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue