Accounts data management (#1530)

* WIP accounts implementation

* Accounts datasore and changes to status mobile API

* Add library changes and method to update config

* Handle error after account selection

* Add two methods to start account to backend

* Use encrypted database for settings and add a service for them

* Resolve linter warning

* Bring back StartNode StopNode for tests

* Add sub accounts and get/save api

* Changes to accounts structure

* Login use root address and fetch necessary info from database

* Cover accounts store with tests

* Refactor in progress

* Initialize status keystore instance before starting ethereum node

* Rework library tests

* Resolve failures in private api test and send transaction test

* Pass pointer to initialized config to unmarshal

* Use multiaccounts/accounts naming consistently

Multiaccount is used as a login identifier
Account references an address and a key, if account is not watch-only.

* Add login timestamp stored in the database to accounts.Account object

* Add photo-path field for multiaccount struct

* Add multiaccoutns rpc with updateAccount method

Update to any other account that wasn't used for login will return an error

* Fix linter in services/accounts

* Select account before starting a node

* Save list of accounts on first login

* Pass account manager to accounts service to avoid selecting account before starting a node

* Add logs to login with save and regualr login
This commit is contained in:
Dmitry Shulyak 2019-08-20 18:38:40 +03:00 committed by GitHub
parent 4f1a3283e6
commit be9c55bc16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1838 additions and 907 deletions

View File

@ -236,7 +236,6 @@ mock-install: ##@other Install mocking tools
mock: ##@other Regenerate mocks
mockgen -package=fcm -destination=notifications/push/fcm/client_mock.go -source=notifications/push/fcm/client.go
mockgen -package=fake -destination=transactions/fake/mock.go -source=transactions/fake/txservice.go
mockgen -package=account -destination=account/accounts_mock.go -source=account/accounts.go
mockgen -package=status -destination=services/status/account_mock.go -source=services/status/service.go
mockgen -package=peer -destination=services/peer/discoverer_mock.go -source=services/peer/service.go

View File

@ -30,21 +30,16 @@ var (
ErrInvalidMasterKeyCreated = errors.New("can not create master extended key")
ErrOnboardingNotStarted = errors.New("onboarding must be started before choosing an account")
ErrOnboardingAccountNotFound = errors.New("cannot find onboarding account with the given id")
ErrAccountKeyStoreMissing = errors.New("account key store is not set")
)
var zeroAddress = common.Address{}
// GethServiceProvider provides required geth services.
type GethServiceProvider interface {
AccountManager() (*accounts.Manager, error)
AccountKeyStore() (*keystore.KeyStore, error)
}
// Manager represents account manager interface.
type Manager struct {
geth GethServiceProvider
mu sync.RWMutex
mu sync.RWMutex
keystore *keystore.KeyStore
manager *accounts.Manager
accountsGenerator *generator.Generator
onboarding *Onboarding
@ -55,15 +50,43 @@ type Manager struct {
}
// NewManager returns new node account manager.
func NewManager(geth GethServiceProvider) *Manager {
manager := &Manager{
geth: geth,
func NewManager() *Manager {
m := &Manager{}
m.accountsGenerator = generator.New(m)
return m
}
// InitKeystore sets key manager and key store.
func (m *Manager) InitKeystore(keydir string) error {
m.mu.Lock()
defer m.mu.Unlock()
manager, err := makeAccountManager(keydir)
if err != nil {
return err
}
m.manager = manager
backends := manager.Backends(keystore.KeyStoreType)
if len(backends) == 0 {
return ErrAccountKeyStoreMissing
}
keyStore, ok := backends[0].(*keystore.KeyStore)
if !ok {
return ErrAccountKeyStoreMissing
}
m.keystore = keyStore
return nil
}
accountsGenerator := generator.New(manager)
manager.accountsGenerator = accountsGenerator
func (m *Manager) GetKeystore() *keystore.KeyStore {
m.mu.RLock()
defer m.mu.RUnlock()
return m.keystore
}
return manager
func (m *Manager) GetManager() *accounts.Manager {
m.mu.RLock()
defer m.mu.RUnlock()
return m.manager
}
// AccountsGenerator returns accountsGenerator.
@ -270,24 +293,22 @@ func (m *Manager) Logout() {
// ImportAccount imports the account specified with privateKey.
func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error) {
keyStore, err := m.geth.AccountKeyStore()
if err != nil {
return common.Address{}, err
if m.keystore == nil {
return common.Address{}, ErrAccountKeyStoreMissing
}
account, err := keyStore.ImportECDSA(privateKey, password)
account, err := m.keystore.ImportECDSA(privateKey, password)
return account.Address, err
}
func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
keyStore, err := m.geth.AccountKeyStore()
if err != nil {
return "", "", err
if m.keystore == nil {
return "", "", ErrAccountKeyStoreMissing
}
// imports extended key, create key file (if necessary)
account, err := keyStore.ImportSingleExtendedKey(extKey, password)
account, err := m.keystore.ImportSingleExtendedKey(extKey, password)
if err != nil {
return "", "", err
}
@ -295,7 +316,7 @@ func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password
address = account.Address.Hex()
// obtain public key to return
account, key, err := keyStore.AccountDecryptedKey(account, password)
account, key, err := m.keystore.AccountDecryptedKey(account, password)
if err != nil {
return address, "", err
}
@ -308,20 +329,19 @@ func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password
// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key.
// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file.
func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
keyStore, err := m.geth.AccountKeyStore()
if err != nil {
return "", "", err
if m.keystore == nil {
return "", "", ErrAccountKeyStoreMissing
}
// imports extended key, create key file (if necessary)
account, err := keyStore.ImportExtendedKeyForPurpose(keyPurpose, extKey, password)
account, err := m.keystore.ImportExtendedKeyForPurpose(keyPurpose, extKey, password)
if err != nil {
return "", "", err
}
address = account.Address.Hex()
// obtain public key to return
account, key, err := keyStore.AccountDecryptedKey(account, password)
account, key, err := m.keystore.AccountDecryptedKey(account, password)
if err != nil {
return address, "", err
}
@ -335,7 +355,6 @@ func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extke
func (m *Manager) Accounts() ([]gethcommon.Address, error) {
m.mu.RLock()
defer m.mu.RUnlock()
addresses := make([]gethcommon.Address, 0)
if m.mainAccountAddress != zeroAddress {
addresses = append(addresses, m.mainAccountAddress)
@ -397,9 +416,8 @@ func (m *Manager) ImportOnboardingAccount(id string, password string) (Info, str
// The running node, has a keystore directory which is loaded on start. Key file
// for a given address is expected to be in that directory prior to node start.
func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) {
keyStore, err := m.geth.AccountKeyStore()
if err != nil {
return accounts.Account{}, nil, err
if m.keystore == nil {
return accounts.Account{}, nil, ErrAccountKeyStoreMissing
}
account, err := ParseAccountString(address)
@ -408,7 +426,7 @@ func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.
}
var key *keystore.Key
account, key, err = keyStore.AccountDecryptedKey(account, password)
account, key, err = m.keystore.AccountDecryptedKey(account, password)
if err != nil {
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)
}

View File

@ -1,65 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: account/accounts.go
// Package account is a generated GoMock package.
package account
import (
accounts "github.com/ethereum/go-ethereum/accounts"
keystore "github.com/ethereum/go-ethereum/accounts/keystore"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockGethServiceProvider is a mock of GethServiceProvider interface
type MockGethServiceProvider struct {
ctrl *gomock.Controller
recorder *MockGethServiceProviderMockRecorder
}
// MockGethServiceProviderMockRecorder is the mock recorder for MockGethServiceProvider
type MockGethServiceProviderMockRecorder struct {
mock *MockGethServiceProvider
}
// NewMockGethServiceProvider creates a new mock instance
func NewMockGethServiceProvider(ctrl *gomock.Controller) *MockGethServiceProvider {
mock := &MockGethServiceProvider{ctrl: ctrl}
mock.recorder = &MockGethServiceProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockGethServiceProvider) EXPECT() *MockGethServiceProviderMockRecorder {
return m.recorder
}
// AccountManager mocks base method
func (m *MockGethServiceProvider) AccountManager() (*accounts.Manager, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AccountManager")
ret0, _ := ret[0].(*accounts.Manager)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AccountManager indicates an expected call of AccountManager
func (mr *MockGethServiceProviderMockRecorder) AccountManager() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountManager", reflect.TypeOf((*MockGethServiceProvider)(nil).AccountManager))
}
// AccountKeyStore mocks base method
func (m *MockGethServiceProvider) AccountKeyStore() (*keystore.KeyStore, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AccountKeyStore")
ret0, _ := ret[0].(*keystore.KeyStore)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AccountKeyStore indicates an expected call of AccountKeyStore
func (mr *MockGethServiceProviderMockRecorder) AccountKeyStore() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountKeyStore", reflect.TypeOf((*MockGethServiceProvider)(nil).AccountKeyStore))
}

View File

@ -9,20 +9,16 @@ import (
"reflect"
"testing"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/golang/mock/gomock"
. "github.com/status-im/status-go/t/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
func TestVerifyAccountPassword(t *testing.T) {
accManager := NewManager(nil)
accManager := NewManager()
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
require.NoError(t, err)
defer os.RemoveAll(keyStoreDir) //nolint: errcheck
@ -108,70 +104,22 @@ func TestVerifyAccountPasswordWithAccountBeforeEIP55(t *testing.T) {
err = ImportTestAccount(keyStoreDir, "test-account3-before-eip55.pk")
require.NoError(t, err)
accManager := NewManager(nil)
accManager := NewManager()
address := gethcommon.HexToAddress(TestConfig.Account3.WalletAddress)
_, err = accManager.VerifyAccountPassword(keyStoreDir, address.Hex(), TestConfig.Account3.Password)
require.NoError(t, err)
}
var errKeyStore = errors.New("Can't return a key store")
func TestManagerTestSuite(t *testing.T) {
gethServiceProvider := newMockGethServiceProvider(t)
accManager := NewManager(gethServiceProvider)
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
require.NoError(t, err)
keyStore := keystore.NewKeyStore(keyStoreDir, keystore.LightScryptN, keystore.LightScryptP)
defer os.RemoveAll(keyStoreDir) //nolint: errcheck
testPassword := "test-password"
// Initial test - create test account
gethServiceProvider.EXPECT().AccountKeyStore().Return(keyStore, nil)
accountInfo, mnemonic, err := accManager.CreateAccount(testPassword)
require.NoError(t, err)
require.NotEmpty(t, accountInfo.WalletAddress)
require.NotEmpty(t, accountInfo.WalletPubKey)
require.NotEmpty(t, accountInfo.ChatAddress)
require.NotEmpty(t, accountInfo.ChatPubKey)
require.NotEmpty(t, mnemonic)
// Before the complete decoupling of the keys, wallet and chat keys are the same
assert.Equal(t, accountInfo.WalletAddress, accountInfo.ChatAddress)
assert.Equal(t, accountInfo.WalletPubKey, accountInfo.ChatPubKey)
s := &ManagerTestSuite{
testAccount: testAccount{
"test-password",
accountInfo.WalletAddress,
accountInfo.WalletPubKey,
accountInfo.ChatAddress,
accountInfo.ChatPubKey,
mnemonic,
},
gethServiceProvider: gethServiceProvider,
accManager: accManager,
keyStore: keyStore,
gethAccManager: accounts.NewManager(),
}
suite.Run(t, s)
}
func newMockGethServiceProvider(t *testing.T) *MockGethServiceProvider {
ctrl := gomock.NewController(t)
return NewMockGethServiceProvider(ctrl)
suite.Run(t, new(ManagerTestSuite))
}
type ManagerTestSuite struct {
suite.Suite
testAccount
gethServiceProvider *MockGethServiceProvider
accManager *Manager
keyStore *keystore.KeyStore
gethAccManager *accounts.Manager
accManager *Manager
keydir string
}
type testAccount struct {
@ -183,43 +131,52 @@ type testAccount struct {
mnemonic string
}
// reinitMock is for reassigning a new mock node manager to account manager.
// Stating the amount of times for mock calls kills the flexibility for
// development so this is a good workaround to use with EXPECT().Func().AnyTimes()
func (s *ManagerTestSuite) reinitMock() {
s.gethServiceProvider = newMockGethServiceProvider(s.T())
s.accManager.geth = s.gethServiceProvider
}
// SetupTest is used here for reinitializing the mock before every
// test function to avoid faulty execution.
func (s *ManagerTestSuite) SetupTest() {
s.reinitMock()
s.accManager = NewManager()
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
s.Require().NoError(err)
s.Require().NoError(s.accManager.InitKeystore(keyStoreDir))
s.keydir = keyStoreDir
testPassword := "test-password"
// Initial test - create test account
accountInfo, mnemonic, err := s.accManager.CreateAccount(testPassword)
s.Require().NoError(err)
s.Require().NotEmpty(accountInfo.WalletAddress)
s.Require().NotEmpty(accountInfo.WalletPubKey)
s.Require().NotEmpty(accountInfo.ChatAddress)
s.Require().NotEmpty(accountInfo.ChatPubKey)
s.Require().NotEmpty(mnemonic)
// Before the complete decoupling of the keys, wallet and chat keys are the same
s.Equal(accountInfo.WalletAddress, accountInfo.ChatAddress)
s.Equal(accountInfo.WalletPubKey, accountInfo.ChatPubKey)
s.testAccount = testAccount{
testPassword,
accountInfo.WalletAddress,
accountInfo.WalletPubKey,
accountInfo.ChatAddress,
accountInfo.ChatPubKey,
mnemonic,
}
}
func (s *ManagerTestSuite) TestCreateAccount() {
// Don't fail on empty password
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
_, _, err := s.accManager.CreateAccount(s.password)
s.NoError(err)
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(nil, errKeyStore)
_, _, err = s.accManager.CreateAccount(s.password)
s.Equal(errKeyStore, err)
func (s *ManagerTestSuite) TearDownTest() {
s.Require().NoError(os.RemoveAll(s.keydir))
}
func (s *ManagerTestSuite) TestRecoverAccount() {
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
accountInfo, err := s.accManager.RecoverAccount(s.password, s.mnemonic)
s.NoError(err)
s.Equal(s.walletAddress, accountInfo.WalletAddress)
s.Equal(s.walletPubKey, accountInfo.WalletPubKey)
s.Equal(s.chatAddress, accountInfo.ChatAddress)
s.Equal(s.chatPubKey, accountInfo.ChatPubKey)
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(nil, errKeyStore)
_, err = s.accManager.RecoverAccount(s.password, s.mnemonic)
s.Equal(errKeyStore, err)
}
func (s *ManagerTestSuite) TestOnboarding() {
@ -240,7 +197,6 @@ func (s *ManagerTestSuite) TestOnboarding() {
// choose one account and encrypt it with password
password := "test-onboarding-account"
account := accounts[0]
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
info, mnemonic, err := s.accManager.ImportOnboardingAccount(account.ID, password)
s.Require().NoError(err)
s.Equal(account.Info, info)
@ -248,7 +204,6 @@ func (s *ManagerTestSuite) TestOnboarding() {
s.Nil(s.accManager.onboarding)
// try to decrypt it with password to check if it's been imported correctly
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
decAccount, _, err := s.accManager.AddressToDecryptedAccount(info.WalletAddress, password)
s.Require().NoError(err)
s.Equal(info.WalletAddress, decAccount.Address.Hex())
@ -262,79 +217,43 @@ func (s *ManagerTestSuite) TestOnboarding() {
s.Nil(s.accManager.onboarding)
}
func (s *ManagerTestSuite) TestSelectAccount() {
testCases := []struct {
name string
accountKeyStoreReturn []interface{}
walletAddress string
chatAddress string
password string
expectedError error
}{
{
"success",
[]interface{}{s.keyStore, nil},
s.walletAddress,
s.chatAddress,
s.password,
nil,
},
{
"fail_keyStore",
[]interface{}{nil, errKeyStore},
s.walletAddress,
s.chatAddress,
s.password,
errKeyStore,
},
{
"fail_wrongChatAddress",
[]interface{}{s.keyStore, nil},
s.walletAddress,
"0x0000000000000000000000000000000000000001",
s.password,
errors.New("cannot retrieve a valid key for a given account: no key for given address or file"),
},
{
"fail_wrongPassword",
[]interface{}{s.keyStore, nil},
s.walletAddress,
s.chatAddress,
"wrong-password",
errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"),
},
func (s *ManagerTestSuite) TestSelectAccountSuccess() {
s.testSelectAccount(common.HexToAddress(s.testAccount.chatAddress), common.HexToAddress(s.testAccount.walletAddress), s.testAccount.password, nil)
}
func (s *ManagerTestSuite) TestSelectAccountWrongAddress() {
s.testSelectAccount(common.HexToAddress("0x0000000000000000000000000000000000000001"), common.HexToAddress(s.testAccount.walletAddress), s.testAccount.password, errors.New("cannot retrieve a valid key for a given account: no key for given address or file"))
}
func (s *ManagerTestSuite) TestSelectAccountWrongPassword() {
s.testSelectAccount(common.HexToAddress(s.testAccount.chatAddress), common.HexToAddress(s.testAccount.walletAddress), "wrong", errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"))
}
func (s *ManagerTestSuite) testSelectAccount(chat, wallet common.Address, password string, expErr error) {
loginParams := LoginParams{
ChatAddress: chat,
MainAccount: wallet,
Password: password,
}
err := s.accManager.SelectAccount(loginParams)
s.Require().Equal(expErr, err)
selectedMainAccountAddress, walletErr := s.accManager.MainAccountAddress()
selectedChatAccount, chatErr := s.accManager.SelectedChatAccount()
if expErr == nil {
s.Require().NoError(walletErr)
s.Require().NoError(chatErr)
s.Equal(wallet, selectedMainAccountAddress)
s.Equal(chat, crypto.PubkeyToAddress(selectedChatAccount.AccountKey.PrivateKey.PublicKey))
} else {
s.Equal(common.Address{}, selectedMainAccountAddress)
s.Nil(selectedChatAccount)
s.Equal(walletErr, ErrNoAccountSelected)
s.Equal(chatErr, ErrNoAccountSelected)
}
for _, testCase := range testCases {
s.T().Run(testCase.name, func(t *testing.T) {
s.reinitMock()
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(testCase.accountKeyStoreReturn...).AnyTimes()
loginParams := LoginParams{
ChatAddress: common.HexToAddress(testCase.chatAddress),
MainAccount: common.HexToAddress(testCase.walletAddress),
Password: testCase.password,
}
err := s.accManager.SelectAccount(loginParams)
s.Equal(testCase.expectedError, err)
selectedMainAccountAddress, walletErr := s.accManager.MainAccountAddress()
selectedChatAccount, chatErr := s.accManager.SelectedChatAccount()
if testCase.expectedError == nil {
s.Equal(testCase.walletAddress, selectedMainAccountAddress.String())
s.Equal(testCase.chatAddress, crypto.PubkeyToAddress(selectedChatAccount.AccountKey.PrivateKey.PublicKey).Hex())
s.NoError(walletErr)
s.NoError(chatErr)
} else {
s.Equal(common.Address{}, selectedMainAccountAddress)
s.Nil(selectedChatAccount)
s.Equal(walletErr, ErrNoAccountSelected)
s.Equal(chatErr, ErrNoAccountSelected)
}
s.accManager.Logout()
})
}
s.accManager.Logout()
}
func (s *ManagerTestSuite) TestSetChatAccount() {
@ -367,7 +286,6 @@ func (s *ManagerTestSuite) TestLogout() {
// TestAccounts tests cases for (*Manager).Accounts.
func (s *ManagerTestSuite) TestAccounts() {
// Select the test account
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil).AnyTimes()
loginParams := LoginParams{
MainAccount: common.HexToAddress(s.walletAddress),
ChatAddress: common.HexToAddress(s.chatAddress),
@ -377,70 +295,36 @@ func (s *ManagerTestSuite) TestAccounts() {
s.NoError(err)
// Success
s.gethServiceProvider.EXPECT().AccountManager().Return(s.gethAccManager, nil)
accs, err := s.accManager.Accounts()
s.NoError(err)
s.NotNil(accs)
// Selected main account address is zero address but doesn't fail
s.accManager.mainAccountAddress = common.Address{}
s.gethServiceProvider.EXPECT().AccountManager().Return(s.gethAccManager, nil)
accs, err = s.accManager.Accounts()
s.NoError(err)
s.NotNil(accs)
}
func (s *ManagerTestSuite) TestAddressToDecryptedAccount() {
testCases := []struct {
name string
accountKeyStoreReturn []interface{}
walletAddress string
password string
expectedError error
}{
{
"success",
[]interface{}{s.keyStore, nil},
s.walletAddress,
s.password,
nil,
},
{
"fail_keyStore",
[]interface{}{nil, errKeyStore},
s.walletAddress,
s.password,
errKeyStore,
},
{
"fail_wrongWalletAddress",
[]interface{}{s.keyStore, nil},
"wrong-wallet-address",
s.password,
ErrAddressToAccountMappingFailure,
},
{
"fail_wrongPassword",
[]interface{}{s.keyStore, nil},
s.walletAddress,
"wrong-password",
errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"),
},
}
func (s *ManagerTestSuite) TestAddressToDecryptedAccountSuccess() {
s.testAddressToDecryptedAccount(s.walletAddress, s.password, nil)
}
for _, testCase := range testCases {
s.T().Run(testCase.name, func(t *testing.T) {
s.reinitMock()
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(testCase.accountKeyStoreReturn...).AnyTimes()
acc, key, err := s.accManager.AddressToDecryptedAccount(testCase.walletAddress, testCase.password)
if testCase.expectedError != nil {
s.Equal(testCase.expectedError, err)
} else {
s.NoError(err)
s.NotNil(acc)
s.NotNil(key)
s.Equal(acc.Address, key.Address)
}
})
func (s *ManagerTestSuite) TestAddressToDecryptedAccountWrongAddress() {
s.testAddressToDecryptedAccount("0x0001", s.password, ErrAddressToAccountMappingFailure)
}
func (s *ManagerTestSuite) TestAddressToDecryptedAccountWrongPassword() {
s.testAddressToDecryptedAccount(s.walletAddress, "wrong", errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"))
}
func (s *ManagerTestSuite) testAddressToDecryptedAccount(wallet, password string, expErr error) {
acc, key, err := s.accManager.AddressToDecryptedAccount(wallet, password)
if expErr != nil {
s.Equal(expErr, err)
} else {
s.Require().NoError(err)
s.Require().NotNil(acc)
s.Require().NotNil(key)
s.Equal(acc.Address, key.Address)
}
}

25
account/keystore.go Normal file
View File

@ -0,0 +1,25 @@
package account
import (
"io/ioutil"
"os"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
)
// makeAccountManager creates ethereum accounts.Manager with single disk backend and lightweight kdf.
// If keydir is empty new temporary directory with go-ethereum-keystore will be intialized.
func makeAccountManager(keydir string) (manager *accounts.Manager, err error) {
if keydir == "" {
// There is no datadir.
keydir, err = ioutil.TempDir("", "go-ethereum-keystore")
}
if err != nil {
return nil, err
}
if err := os.MkdirAll(keydir, 0700); err != nil {
return nil, err
}
return accounts.NewManager(keystore.NewKeyStore(keydir, keystore.LightScryptN, keystore.LightScryptP)), nil
}

View File

@ -42,7 +42,6 @@ func ParseLoginParams(paramsJSON string) (LoginParams, error) {
params LoginParams
zeroAddress common.Address
)
if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
return params, err
}
@ -60,7 +59,6 @@ func ParseLoginParams(paramsJSON string) (LoginParams, error) {
return params, newErrZeroAddress("WatchAddresses")
}
}
return params, nil
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"math/big"
"path"
"path/filepath"
"sync"
"time"
@ -19,11 +20,15 @@ import (
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/crypto"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/mailserver/registry"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/node"
"github.com/status-im/status-go/notifications/push/fcm"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
accountssvc "github.com/status-im/status-go/services/accounts"
"github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/rpcfilters"
"github.com/status-im/status-go/services/subscriptions"
@ -52,10 +57,14 @@ var (
// StatusBackend implements Status.im service
type StatusBackend struct {
mu sync.Mutex
mu sync.Mutex
// rootDataDir is the same for all networks.
rootDataDir string
accountsDB *accounts.Database
statusNode *node.StatusNode
personalAPI *personal.PublicAPI
rpcFilters *rpcfilters.Service
multiaccountsDB *multiaccounts.Database
accountManager *account.Manager
transactor *transactions.Transactor
newNotification fcm.NotificationConstructor
@ -70,12 +79,11 @@ func NewStatusBackend() *StatusBackend {
defer log.Info("Status backend initialized", "version", params.Version, "commit", params.GitCommit)
statusNode := node.New()
accountManager := account.NewManager(statusNode)
accountManager := account.NewManager()
transactor := transactions.NewTransactor()
personalAPI := personal.NewAPI()
notificationManager := fcm.NewNotification(fcmServerKey)
rpcFilters := rpcfilters.New(statusNode)
return &StatusBackend{
statusNode: statusNode,
accountManager: accountManager,
@ -121,6 +129,144 @@ func (b *StatusBackend) StartNode(config *params.NodeConfig) error {
return nil
}
func (b *StatusBackend) UpdateRootDataDir(datadir string) {
b.mu.Lock()
defer b.mu.Unlock()
b.rootDataDir = datadir
}
func (b *StatusBackend) OpenAccounts() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB != nil {
return nil
}
db, err := multiaccounts.InitializeDB(filepath.Join(b.rootDataDir, "accounts.sql"))
if err != nil {
return err
}
b.multiaccountsDB = db
return nil
}
func (b *StatusBackend) GetAccounts() ([]multiaccounts.Account, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return nil, errors.New("accoutns db wasn't initialized")
}
return b.multiaccountsDB.GetAccounts()
}
func (b *StatusBackend) SaveAccount(account multiaccounts.Account) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return errors.New("accoutns db wasn't initialized")
}
return b.multiaccountsDB.SaveAccount(account)
}
func (b *StatusBackend) ensureAccountsDBOpened(account multiaccounts.Account, password string) (err error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.accountsDB != nil {
return nil
}
if len(b.rootDataDir) == 0 {
return errors.New("root datadir wasn't provided")
}
path := filepath.Join(b.rootDataDir, fmt.Sprintf("accounts-%x.sql", account.Address))
b.accountsDB, err = accounts.InitializeDB(path, password)
return err
}
func (b *StatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string) error {
err := b.ensureAccountsDBOpened(acc, password)
if err != nil {
return err
}
conf, err := b.loadNodeConfig()
if err != nil {
return err
}
if err := logutils.OverrideRootLogWithConfig(conf, false); err != nil {
return err
}
chatAddr, err := b.accountsDB.GetChatAddress()
if err != nil {
return err
}
walletAddr, err := b.accountsDB.GetWalletAddress()
if err != nil {
return err
}
watchAddrs, err := b.accountsDB.GetAddresses()
if err != nil {
return err
}
login := account.LoginParams{
Password: password,
ChatAddress: chatAddr,
WatchAddresses: watchAddrs,
MainAccount: walletAddr,
}
err = b.StartNode(conf)
if err != nil {
return err
}
err = b.SelectAccount(login)
if err != nil {
return err
}
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.Address, time.Now().Unix())
if err != nil {
return err
}
signal.SendLoggedIn()
return nil
}
// StartNodeWithAccountAndConfig is used after account and config was generated.
// In current setup account name and config is generated on the client side. Once/if it will be generated on
// status-go side this flow can be simplified.
func (b *StatusBackend) StartNodeWithAccountAndConfig(account multiaccounts.Account, password string, conf *params.NodeConfig, subaccs []accounts.Account) error {
err := b.SaveAccount(account)
if err != nil {
return err
}
err = b.ensureAccountsDBOpened(account, password)
if err != nil {
return err
}
err = b.saveNodeConfig(conf)
if err != nil {
return err
}
err = b.accountsDB.SaveAccounts(subaccs)
if err != nil {
return err
}
return b.StartNodeWithAccount(account, password)
}
func (b *StatusBackend) saveNodeConfig(config *params.NodeConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
return b.accountsDB.SaveConfig(accounts.NodeConfigTag, config)
}
func (b *StatusBackend) loadNodeConfig() (*params.NodeConfig, error) {
b.mu.Lock()
defer b.mu.Unlock()
conf := params.NodeConfig{}
err := b.accountsDB.GetConfig(accounts.NodeConfigTag, &conf)
if err != nil {
return nil, err
}
return &conf, nil
}
func (b *StatusBackend) rpcFiltersService() gethnode.ServiceConstructor {
return func(*gethnode.ServiceContext) (gethnode.Service, error) {
return rpcfilters.New(b.statusNode), nil
@ -133,6 +279,12 @@ func (b *StatusBackend) subscriptionService() gethnode.ServiceConstructor {
}
}
func (b *StatusBackend) accountsService() gethnode.ServiceConstructor {
return func(*gethnode.ServiceContext) (gethnode.Service, error) {
return accountssvc.NewService(b.accountsDB, b.multiaccountsDB, b.AccountManager()), nil
}
}
func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
defer func() {
if r := recover(); r != nil {
@ -144,17 +296,22 @@ func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
if err := config.Validate(); err != nil {
return err
}
services := []gethnode.ServiceConstructor{}
services = appendIf(config.UpstreamConfig.Enabled, services, b.rpcFiltersService())
services = append(services, b.subscriptionService())
services = appendIf(b.accountsDB != nil, services, b.accountsService())
manager := b.accountManager.GetManager()
if manager == nil {
return errors.New("ethereum accounts.Manager is nil")
}
if err = b.statusNode.StartWithOptions(config, node.StartOptions{
Services: services,
// The peers discovery protocols are started manually after
// `node.ready` signal is sent.
// It was discussed in https://github.com/status-im/status-go/pull/1333.
StartDiscovery: false,
StartDiscovery: false,
AccountsManager: manager,
}); err != nil {
return
}
@ -484,6 +641,22 @@ func (b *StatusBackend) Logout() error {
b.mu.Lock()
defer b.mu.Unlock()
err := b.cleanupServices()
if err != nil {
return err
}
err = b.stopAccountsDB()
if err != nil {
return err
}
b.AccountManager().Logout()
return nil
}
// cleanupServices stops parts of services that doesn't managed by a node and removes injected data from services.
func (b *StatusBackend) cleanupServices() error {
whisperService, err := b.statusNode.WhisperService()
switch err {
case node.ErrServiceUnknown: // Whisper was never registered
@ -521,9 +694,15 @@ func (b *StatusBackend) Logout() error {
return err
}
}
return nil
}
b.AccountManager().Logout()
func (b *StatusBackend) stopAccountsDB() error {
if b.accountsDB != nil {
err := b.accountsDB.Close()
b.accountsDB = nil
return err
}
return nil
}
@ -622,7 +801,6 @@ func (b *StatusBackend) startWallet(password string) error {
allAddresses := make([]common.Address, len(watchAddresses)+1)
allAddresses[0] = mainAccountAddress
copy(allAddresses[1:], watchAddresses)
path := path.Join(b.statusNode.Config().DataDir, fmt.Sprintf("wallet-%x.sql", mainAccountAddress))
return wallet.StartReactor(path, password,
b.statusNode.RPCClient().Ethclient(),

View File

@ -8,6 +8,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/node"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/signal"
"github.com/status-im/status-go/t/utils"
@ -23,11 +24,16 @@ const (
func TestSubscriptionEthWithParamsDict(t *testing.T) {
// a simple test to check the parameter parsing for eth_* filter subscriptions
backend := NewStatusBackend()
// initNodeAndLogin can fail and terminate the test, in that case stopNode must be executed anyway.
defer func() {
err := backend.StopNode()
if err != node.ErrNoRunningNode {
require.NoError(t, err)
}
}()
initNodeAndLogin(t, backend)
defer func() { require.NoError(t, backend.StopNode()) }()
createSubscription(t, backend, fmt.Sprintf(`"eth_newFilter", [
{
"fromBlock":"earliest",
@ -40,11 +46,15 @@ func TestSubscriptionEthWithParamsDict(t *testing.T) {
func TestSubscriptionPendingTransaction(t *testing.T) {
backend := NewStatusBackend()
backend.allowAllRPC = true
defer func() {
err := backend.StopNode()
if err != node.ErrNoRunningNode {
require.NoError(t, err)
}
}()
account, _ := initNodeAndLogin(t, backend)
defer func() { require.NoError(t, backend.StopNode()) }()
signals := make(chan string)
defer func() {
signal.ResetDefaultNodeNotificationHandler()
@ -88,11 +98,15 @@ func TestSubscriptionPendingTransaction(t *testing.T) {
func TestSubscriptionWhisperEnvelopes(t *testing.T) {
backend := NewStatusBackend()
defer func() {
err := backend.StopNode()
if err != node.ErrNoRunningNode {
require.NoError(t, err)
}
}()
initNodeAndLogin(t, backend)
defer func() { require.NoError(t, backend.StopNode()) }()
signals := make(chan string)
defer func() {
signal.ResetDefaultNodeNotificationHandler()
@ -222,9 +236,9 @@ func initNodeAndLogin(t *testing.T, backend *StatusBackend) (string, string) {
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
info, _, err := backend.AccountManager().CreateAccount(password)
require.NoError(t, err)

View File

@ -25,6 +25,7 @@ func TestBackendStartNodeConcurrently(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
count := 2
resultCh := make(chan error)
@ -58,9 +59,8 @@ func TestBackendRestartNodeConcurrently(t *testing.T) {
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
count := 3
err = backend.StartNode(config)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
require.NoError(t, backend.StartNode(config))
defer func() {
require.NoError(t, backend.StopNode())
}()
@ -84,7 +84,7 @@ func TestBackendGettersConcurrently(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
@ -136,7 +136,7 @@ func TestBackendAccountsConcurrently(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
@ -196,6 +196,7 @@ func TestBackendInjectChatAccount(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
@ -269,6 +270,7 @@ func TestBackendCallRPCConcurrently(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
count := 3
err = backend.StartNode(config)
@ -342,6 +344,7 @@ func TestBlockedRPCMethods(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() { require.NoError(t, backend.StopNode()) }()
@ -379,6 +382,7 @@ func TestStartStopMultipleTimes(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
config.NoDiscovery = false
// doesn't have to be running. just any valid enode to bypass validation.
config.ClusterConfig.BootNodes = []string{
@ -395,6 +399,7 @@ func TestSignHash(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
require.NoError(t, backend.StartNode(config))
defer func() {
@ -432,6 +437,7 @@ func TestHashTypedData(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {

View File

@ -16,6 +16,7 @@ func TestHashMessage(t *testing.T) {
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {

View File

@ -12,10 +12,11 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/api"
"github.com/status-im/status-go/exportlogs"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/profiling"
"github.com/status-im/status-go/services/personal"
@ -28,28 +29,23 @@ import (
// All general log messages in this package should be routed through this logger.
var logger = log.New("package", "status-go/lib")
//StartNode - start Status node
//export StartNode
func StartNode(configJSON *C.char) *C.char {
config, err := params.NewConfigFromJSON(C.GoString(configJSON))
// OpenAccounts opens database and returns accounts list.
//export OpenAccounts
func OpenAccounts(datadir *C.char) *C.char {
statusBackend.UpdateRootDataDir(C.GoString(datadir))
err := statusBackend.OpenAccounts()
if err != nil {
return makeJSONResponse(err)
}
if err := logutils.OverrideRootLogWithConfig(config, false); err != nil {
accs, err := statusBackend.GetAccounts()
if err != nil {
return makeJSONResponse(err)
}
api.RunAsync(func() error { return statusBackend.StartNode(config) })
return makeJSONResponse(nil)
}
//StopNode - stop status node
//export StopNode
func StopNode() *C.char {
api.RunAsync(statusBackend.StopNode)
return makeJSONResponse(nil)
data, err := json.Marshal(accs)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(data))
}
// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature
@ -324,16 +320,72 @@ func VerifyAccountPassword(keyStoreDir, address, password *C.char) *C.char {
return makeJSONResponse(err)
}
//StartNode - start Status node
//export StartNode
func StartNode(configJSON *C.char) *C.char {
config, err := params.NewConfigFromJSON(C.GoString(configJSON))
if err != nil {
return makeJSONResponse(err)
}
if err := logutils.OverrideRootLogWithConfig(config, false); err != nil {
return makeJSONResponse(err)
}
api.RunAsync(func() error { return statusBackend.StartNode(config) })
return makeJSONResponse(nil)
}
//StopNode - stop status node
//export StopNode
func StopNode() *C.char {
api.RunAsync(statusBackend.StopNode)
return makeJSONResponse(nil)
}
//Login loads a key file (for a given address), tries to decrypt it using the password, to verify ownership
// if verified, purges all the previous identities from Whisper, and injects verified key as shh identity
//export Login
func Login(loginParamsJSON *C.char) *C.char {
params, err := account.ParseLoginParams(C.GoString(loginParamsJSON))
func Login(accountData, password *C.char) *C.char {
data, pass := C.GoString(accountData), C.GoString(password)
var account multiaccounts.Account
err := json.Unmarshal([]byte(data), &account)
if err != nil {
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
return makeJSONResponse(err)
}
api.RunAsync(func() error { return statusBackend.StartNodeWithAccount(account, pass) })
return makeJSONResponse(nil)
}
err = statusBackend.SelectAccount(params)
// SaveAccountAndLogin saves account in status-go database..
//export SaveAccountAndLogin
func SaveAccountAndLogin(accountData, password, configJSON, subaccountData *C.char) *C.char {
data, confJSON, subData := C.GoString(accountData), C.GoString(configJSON), C.GoString(subaccountData)
var account multiaccounts.Account
err := json.Unmarshal([]byte(data), &account)
if err != nil {
return makeJSONResponse(err)
}
conf := params.NodeConfig{}
err = json.Unmarshal([]byte(confJSON), &conf)
if err != nil {
return makeJSONResponse(err)
}
var subaccs []accounts.Account
err = json.Unmarshal([]byte(subData), &subaccs)
if err != nil {
return makeJSONResponse(err)
}
api.RunAsync(func() error {
return statusBackend.StartNodeWithAccountAndConfig(account, C.GoString(password), &conf, subaccs)
})
return makeJSONResponse(nil)
}
// InitKeystore initialize keystore before doing any operations with keys.
//export InitKeystore
func InitKeystore(keydir *C.char) *C.char {
err := statusBackend.AccountManager().InitKeystore(C.GoString(keydir))
return makeJSONResponse(err)
}
@ -349,7 +401,11 @@ func LoginWithKeycard(chatKeyData, encryptionKeyData *C.char) *C.char {
//export Logout
func Logout() *C.char {
err := statusBackend.Logout()
return makeJSONResponse(err)
if err != nil {
makeJSONResponse(err)
}
api.RunAsync(statusBackend.StopNode)
return makeJSONResponse(nil)
}
// SignMessage unmarshals rpc params {data, address, password} and passes

View File

@ -16,10 +16,7 @@ import (
// the actual test functions are in non-_test.go files (so that they can use cgo i.e. import "C")
// the only intent of these wrappers is for gotest can find what tests are exposed.
func TestExportedAPI(t *testing.T) {
allTestsDone := make(chan struct{}, 1)
go testExportedAPI(t, allTestsDone)
<-allTestsDone
testExportedAPI(t)
}
func TestValidateNodeConfig(t *testing.T) {

View File

@ -14,6 +14,7 @@ import (
"strings"
"testing"
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/account/generator"
)
@ -50,12 +51,7 @@ func checkMultiAccountResponse(t *testing.T, respJSON *C.char, resp interface{})
}
}
func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
params := C.CString(`{
"n": 2,
"mnemonicPhraseLength": 24,
@ -90,7 +86,6 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
return false
}
}
password := "multi-account-test-password"
// store 2 derived child accounts from the first account.
@ -115,9 +110,7 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
return false
}
}
rawResp = MultiAccountReset()
// try again deriving addresses.
// it should fail because reset should remove all the accounts from memory.
for _, loadedID := range loadedIDs {
@ -126,16 +119,10 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
return false
}
}
return true
}
func testMultiAccountImportMnemonicAndDerive(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
func testMultiAccountImportMnemonicAndDerive(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
mnemonicPhrase := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
bip39Passphrase := "TREZOR"
params := mobile.MultiAccountImportMnemonicParams{
@ -210,7 +197,6 @@ func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []str
addresses[path] = info.Address
}
return addresses, true
}
@ -247,7 +233,8 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, password strin
}
// for each stored account, check that we can decrypt it with the password we used.
dir := statusBackend.StatusNode().Config().DataDir
// FIXME pass it somehow
dir := keystoreDir
for _, address := range addresses {
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password)
if err != nil {
@ -259,12 +246,7 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, password strin
return addresses, true
}
func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
func testMultiAccountGenerateAndDerive(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
paths := []string{"m/0", "m/1"}
params := mobile.MultiAccountGenerateAndDeriveAddressesParams{
MultiAccountGenerateParams: mobile.MultiAccountGenerateParams{
@ -303,12 +285,7 @@ func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
return true
}
func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
func testMultiAccountImportStore(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
key, err := crypto.GenerateKey()
if err != nil {
t.Errorf("failed generating key")
@ -350,7 +327,7 @@ func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
// check the response doesn't have errors
checkMultiAccountResponse(t, rawResp, &storeResp)
dir := statusBackend.StatusNode().Config().DataDir
dir := keystoreDir
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, storeResp.Address, password)
if err != nil {
t.Errorf("failed to verify password on stored derived account")

View File

@ -9,8 +9,8 @@
package main
import "C"
import (
"C"
"encoding/hex"
"encoding/json"
"fmt"
@ -31,10 +31,14 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/signal"
. "github.com/status-im/status-go/t/utils" //nolint: golint
"github.com/status-im/status-go/transactions"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/event"
)
import "github.com/status-im/status-go/multiaccounts/accounts"
const initJS = `
var _status_catalog = {
@ -44,15 +48,27 @@ const initJS = `
var (
zeroHash = gethcommon.Hash{}
testChainDir string
keystoreDir string
nodeConfigJSON string
)
func buildLoginParamsJSON(chatAddress, password string) *C.char {
func buildAccountData(name, chatAddress string) *C.char {
return C.CString(fmt.Sprintf(`{
"chatAddress": "%s",
"password": "%s",
"mainAccount": "%s"
}`, chatAddress, password, chatAddress))
"name": "%s",
"address": "%s"
}`, name, chatAddress))
}
func buildSubAccountData(chatAddress string) *C.char {
accs := []accounts.Account{
{
Wallet: true,
Chat: true,
Address: gethcommon.HexToAddress(chatAddress),
},
}
data, _ := json.Marshal(accs)
return C.CString(string(data))
}
func buildLoginParams(mainAccountAddress, chatAddress, password string) account.LoginParams {
@ -63,8 +79,26 @@ func buildLoginParams(mainAccountAddress, chatAddress, password string) account.
}
}
func waitSignal(feed *event.Feed, event string, timeout time.Duration) error {
events := make(chan signal.Envelope)
sub := feed.Subscribe(events)
defer sub.Unsubscribe()
after := time.After(timeout)
for {
select {
case envelope := <-events:
if envelope.Type == event {
return nil
}
case <-after:
return fmt.Errorf("signal %v wasn't received in %v", event, timeout)
}
}
}
func init() {
testChainDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
keystoreDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()], "keystore")
nodeConfigJSON = `{
"NetworkId": ` + strconv.Itoa(GetNetworkID()) + `,
@ -73,6 +107,7 @@ func init() {
"HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `,
"LogLevel": "INFO",
"NoDiscovery": true,
"APIModules": "web3,eth",
"LightEthConfig": {
"Enabled": true
},
@ -87,106 +122,123 @@ func init() {
}`
}
// nolint: deadcode
func testExportedAPI(t *testing.T, done chan struct{}) {
<-startTestNode(t)
defer func() {
done <- struct{}{}
}()
func createAccountAndLogin(t *testing.T, feed *event.Feed) account.Info {
account1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
require.NoError(t, err)
t.Logf("account created: {address: %s, key: %s}", account1.WalletAddress, account1.WalletPubKey)
// prepare accounts
testKeyDir := filepath.Join(testChainDir, "keystore")
if err := ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil {
panic(err)
}
if err := ImportTestAccount(testKeyDir, GetAccount2PKFile()); err != nil {
panic(err)
}
// select account
loginResponse := APIResponse{}
rawResponse := SaveAccountAndLogin(buildAccountData("test", account1.WalletAddress), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(account1.WalletAddress))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
require.Empty(t, loginResponse.Error)
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
return account1
}
// nolint: deadcode
func testExportedAPI(t *testing.T) {
testDir := filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
_ = OpenAccounts(C.CString(testDir))
// inject test accounts
testKeyDir := filepath.Join(testDir, "keystore")
_ = InitKeystore(C.CString(testKeyDir))
require.NoError(t, ImportTestAccount(testKeyDir, GetAccount1PKFile()))
require.NoError(t, ImportTestAccount(testKeyDir, GetAccount2PKFile()))
// FIXME(tiabc): All of that is done because usage of cgo is not supported in tests.
// Probably, there should be a cleaner way, for example, test cgo bindings in e2e tests
// separately from other internal tests.
// FIXME(@jekamas): ATTENTION! this tests depends on each other!
// NOTE(dshulyak) tests are using same backend with same keystore. but after every test we explicitly logging out.
tests := []struct {
name string
fn func(t *testing.T) bool
fn func(t *testing.T, feed *event.Feed) bool
}{
{
"stop/resume node",
"StopResumeNode",
testStopResumeNode,
},
{
"call RPC on in-proc handler",
"RPCInProc",
testCallRPC,
},
{
"call private API using RPC",
"RPCPrivateAPI",
testCallRPCWithPrivateAPI,
},
{
"call private API using private RPC client",
"RPCPrivateClient",
testCallPrivateRPCWithPrivateAPI,
},
{
"verify account password",
"VerifyAccountPassword",
testVerifyAccountPassword,
},
{
"recover account",
"RecoverAccount",
testRecoverAccount,
},
{
"account select/login",
testAccountSelect,
},
{
"login with keycard",
"LoginKeycard",
testLoginWithKeycard,
},
{
"account logout",
"AccountLoout",
testAccountLogout,
},
{
"send transaction",
testSendTransaction,
"SendTransaction",
testSendTransactionWithLogin,
},
{
"send transaction with invalid password",
"SendTransactionInvalidPassword",
testSendTransactionInvalidPassword,
},
{
"failed single transaction",
"SendTransactionFailed",
testFailedTransaction,
},
{
"MultiAccount - Generate/Derive/StoreDerived/Load/Reset",
"MultiAccount/Generate/Derive/StoreDerived/Load/Reset",
testMultiAccountGenerateDeriveStoreLoadReset,
},
{
"MultiAccount - ImportMnemonic/Derive",
"MultiAccount/ImportMnemonic/Derive",
testMultiAccountImportMnemonicAndDerive,
},
{
"MultiAccount - GenerateAndDerive",
"MultiAccount/GenerateAndDerive",
testMultiAccountGenerateAndDerive,
},
{
"MultiAccount - Import/Store",
"MultiAccount/Import/Store",
testMultiAccountImportStore,
},
}
for _, test := range tests {
t.Logf("=== RUN %s", test.name)
if ok := test.fn(t); !ok {
t.Logf("=== FAILED %s", test.name)
break
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
feed := &event.Feed{}
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope signal.Envelope
require.NoError(t, json.Unmarshal([]byte(jsonEvent), &envelope))
feed.Send(envelope)
})
defer func() {
if snode := statusBackend.StatusNode(); snode == nil || !snode.IsRunning() {
return
}
Logout()
waitSignal(feed, signal.EventNodeStopped, 5*time.Second)
}()
require.True(t, tc.fn(t, feed))
})
}
}
func testVerifyAccountPassword(t *testing.T) bool {
func testVerifyAccountPassword(t *testing.T, feed *event.Feed) bool {
tmpDir, err := ioutil.TempDir(os.TempDir(), "accounts")
if err != nil {
t.Fatal(err)
@ -225,137 +277,34 @@ func testVerifyAccountPassword(t *testing.T) bool {
return true
}
//@TODO(adam): quarantined this test until it uses a different directory.
//nolint: deadcode
func testResetChainData(t *testing.T) bool {
t.Skip()
resetChainDataResponse := APIResponse{}
rawResponse := ResetChainData()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &resetChainDataResponse); err != nil {
t.Errorf("cannot decode ResetChainData response (%s): %v", C.GoString(rawResponse), err)
return false
}
if resetChainDataResponse.Error != "" {
t.Errorf("unexpected error: %s", resetChainDataResponse.Error)
return false
}
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
testSendTransaction(t)
return true
}
func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
func testStopResumeNode(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
account1 := createAccountAndLogin(t, feed)
whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
require.NoError(t, err)
require.True(t, whisperService.HasKeyPair(account1.ChatPubKey), "whisper should have keypair")
// create an account
account1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
t.Logf("account created: {address: %s, key: %s}", account1.WalletAddress, account1.WalletPubKey)
response := APIResponse{}
rawResponse := StopNode()
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
require.Empty(t, response.Error)
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(account1.ChatPubKey) {
t.Error("identity already present in whisper")
}
require.NoError(t, waitSignal(feed, signal.EventNodeStopped, 3*time.Second))
response = APIResponse{}
rawResponse = StartNode(C.CString(nodeConfigJSON))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
require.Empty(t, response.Error)
// select account
loginResponse := APIResponse{}
rawResponse := Login(buildLoginParamsJSON(account1.WalletAddress, TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error != "" {
t.Errorf("could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(account1.ChatPubKey) {
t.Errorf("identity not injected into whisper: %v", err)
}
// stop and resume node, then make sure that selected account is still selected
// nolint: dupl
stopNodeFn := func() bool {
response := APIResponse{}
// FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid
// 9-sec timeout below after stopping the node.
rawResponse = StopNode()
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
t.Errorf("cannot decode StopNode response (%s): %v", C.GoString(rawResponse), err)
return false
}
if response.Error != "" {
t.Errorf("unexpected error: %s", response.Error)
return false
}
return true
}
// nolint: dupl
resumeNodeFn := func() bool {
response := APIResponse{}
// FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid
// 10-sec timeout below after resuming the node.
rawResponse = StartNode(C.CString(nodeConfigJSON))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
t.Errorf("cannot decode StartNode response (%s): %v", C.GoString(rawResponse), err)
return false
}
if response.Error != "" {
t.Errorf("unexpected error: %s", response.Error)
return false
}
return true
}
if !stopNodeFn() {
return false
}
time.Sleep(9 * time.Second) // allow to stop
if !resumeNodeFn() {
return false
}
time.Sleep(10 * time.Second) // allow to start (instead of using blocking version of start, of filter event)
require.NoError(t, waitSignal(feed, signal.EventNodeReady, 5*time.Second))
// now, verify that we still have account logged in
whisperService, err = statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
if !whisperService.HasKeyPair(account1.ChatPubKey) {
t.Errorf("identity evicted from whisper on node restart: %v", err)
}
// additionally, let's complete transaction (just to make sure that node lives through pause/resume w/o issues)
testSendTransaction(t)
require.NoError(t, err)
require.True(t, whisperService.HasKeyPair(account1.ChatPubKey))
return true
}
func testCallRPC(t *testing.T) bool {
func testCallRPC(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
expected := `{"jsonrpc":"2.0","id":64,"result":"0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"}`
rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f20776f726c64"],"id":64}`))
received := C.GoString(rawResponse)
@ -367,19 +316,16 @@ func testCallRPC(t *testing.T) bool {
return true
}
func testCallRPCWithPrivateAPI(t *testing.T) bool {
func testCallRPCWithPrivateAPI(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
expected := `{"jsonrpc":"2.0","id":64,"error":{"code":-32601,"message":"The method admin_nodeInfo does not exist/is not available"}}`
rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
received := C.GoString(rawResponse)
if expected != received {
t.Errorf("unexpected response: expected: %v, got: %v", expected, received)
return false
}
require.Equal(t, expected, C.GoString(rawResponse))
return true
}
func testCallPrivateRPCWithPrivateAPI(t *testing.T) bool {
func testCallPrivateRPCWithPrivateAPI(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
rawResponse := CallPrivateRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
received := C.GoString(rawResponse)
if strings.Contains(received, "error") {
@ -390,9 +336,9 @@ func testCallPrivateRPCWithPrivateAPI(t *testing.T) bool {
return true
}
func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
keyStore, _ := statusBackend.StatusNode().AccountKeyStore()
func testRecoverAccount(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
keyStore := statusBackend.AccountManager().GetKeystore()
require.NotNil(t, keyStore)
// create an account
accountInfo, mnemonic, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
if err != nil {
@ -503,21 +449,18 @@ func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
t.Error("recover chat account details failed to pull the correct details (for non-cached account)")
}
loginResponse := APIResponse{}
rawResponse = SaveAccountAndLogin(buildAccountData("test", walletAddressCheck), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(walletAddressCheck))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
require.Empty(t, loginResponse.Error)
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
// time to login with recovered data
whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(chatPubKeyCheck) {
t.Error("identity already present in whisper")
}
err = statusBackend.SelectAccount(buildLoginParams(walletAddressCheck, chatAddressCheck, TestConfig.Account1.Password))
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(chatPubKeyCheck) {
t.Errorf("identity not injected into whisper: %v", err)
}
@ -525,91 +468,8 @@ func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
return true
}
func testAccountSelect(t *testing.T) bool { //nolint: gocyclo
// test to see if the account was injected in whisper
whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create an account
accountInfo1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
t.Logf("Account created: {address: %s, key: %s}", accountInfo1.WalletAddress, accountInfo1.WalletPubKey)
accountInfo2, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Error("Test failed: could not create account")
return false
}
t.Logf("Account created: {address: %s, key: %s}", accountInfo2.WalletAddress, accountInfo2.WalletPubKey)
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(accountInfo1.ChatPubKey) {
t.Error("identity already present in whisper")
}
// try selecting with wrong password
loginResponse := APIResponse{}
rawResponse := Login(buildLoginParamsJSON(accountInfo1.WalletAddress, "wrongPassword"))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error == "" {
t.Error("select account is expected to throw error: wrong password used")
return false
}
loginResponse = APIResponse{}
rawResponse = Login(buildLoginParamsJSON(accountInfo1.WalletAddress, TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error != "" {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(accountInfo1.ChatPubKey) {
t.Errorf("identity not injected into whisper: %v", err)
}
// select another account, make sure that previous account is wiped out from Whisper cache
if whisperService.HasKeyPair(accountInfo2.ChatPubKey) {
t.Error("identity already present in whisper")
}
loginResponse = APIResponse{}
rawResponse = Login(buildLoginParamsJSON(accountInfo2.WalletAddress, TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error != "" {
t.Errorf("Test failed: could not select account: %v", loginResponse.Error)
return false
}
if !whisperService.HasKeyPair(accountInfo2.ChatPubKey) {
t.Errorf("identity not injected into whisper: %v", err)
}
if whisperService.HasKeyPair(accountInfo1.ChatPubKey) {
t.Error("identity should be removed, but it is still present in whisper")
}
return true
}
func testLoginWithKeycard(t *testing.T) bool { //nolint: gocyclo
func testLoginWithKeycard(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
createAccountAndLogin(t, feed)
chatPrivKey, err := crypto.GenerateKey()
if err != nil {
t.Errorf("error generating chat key")
@ -656,32 +516,14 @@ func testLoginWithKeycard(t *testing.T) bool { //nolint: gocyclo
return true
}
func testAccountLogout(t *testing.T) bool {
func testAccountLogout(t *testing.T, feed *event.Feed) bool {
accountInfo := createAccountAndLogin(t, feed)
whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
return false
}
// create an account
accountInfo, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
// make sure that identity doesn't exist (yet) in Whisper
if whisperService.HasKeyPair(accountInfo.ChatPubKey) {
t.Error("identity already present in whisper")
return false
}
// select/login
err = statusBackend.SelectAccount(buildLoginParams(accountInfo.WalletAddress, accountInfo.ChatAddress, TestConfig.Account1.Password))
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(accountInfo.ChatPubKey) {
t.Error("identity not injected into whisper")
return false
@ -714,15 +556,14 @@ type jsonrpcAnyResponse struct {
jsonrpcErrorResponse
}
func testSendTransaction(t *testing.T) bool {
func testSendTransactionWithLogin(t *testing.T, feed *event.Feed) bool {
loginResponse := APIResponse{}
rawResponse := SaveAccountAndLogin(buildAccountData("test", TestConfig.Account1.WalletAddress), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(TestConfig.Account1.WalletAddress))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
require.Empty(t, loginResponse.Error)
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
// log into account from which transactions will be sent
if err := statusBackend.SelectAccount(buildLoginParams(TestConfig.Account1.WalletAddress, TestConfig.Account1.ChatAddress, TestConfig.Account1.Password)); err != nil {
t.Errorf("cannot select account: %v. Error %q", TestConfig.Account1.WalletAddress, err)
return false
}
args, err := json.Marshal(transactions.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.WalletAddress),
To: account.ToAddress(TestConfig.Account2.WalletAddress),
@ -752,7 +593,8 @@ func testSendTransaction(t *testing.T) bool {
return true
}
func testSendTransactionInvalidPassword(t *testing.T) bool {
func testSendTransactionInvalidPassword(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
// log into account from which transactions will be sent
@ -789,7 +631,8 @@ func testSendTransactionInvalidPassword(t *testing.T) bool {
return true
}
func testFailedTransaction(t *testing.T) bool {
func testFailedTransaction(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
// log into wrong account in order to get selectedAccount error
@ -829,70 +672,6 @@ func testFailedTransaction(t *testing.T) bool {
}
func startTestNode(t *testing.T) <-chan struct{} {
testDir := filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
syncRequired := false
if _, err := os.Stat(testDir); os.IsNotExist(err) {
syncRequired = true
}
// inject test accounts
testKeyDir := filepath.Join(testDir, "keystore")
if err := ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil {
panic(err)
}
if err := ImportTestAccount(testKeyDir, GetAccount2PKFile()); err != nil {
panic(err)
}
waitForNodeStart := make(chan struct{}, 1)
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
t.Log(jsonEvent)
var envelope signal.Envelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == signal.EventNodeCrashed {
signal.TriggerDefaultNodeNotificationHandler(jsonEvent)
return
}
if envelope.Type == signal.EventSignRequestAdded {
}
if envelope.Type == signal.EventNodeStarted {
t.Log("Node started, but we wait till it be ready")
}
if envelope.Type == signal.EventNodeReady {
// sync
if syncRequired {
t.Logf("Sync is required")
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
} else {
time.Sleep(5 * time.Second)
}
// now we can proceed with tests
waitForNodeStart <- struct{}{}
}
})
go func() {
response := StartNode(C.CString(nodeConfigJSON))
responseErr := APIResponse{}
if err := json.Unmarshal([]byte(C.GoString(response)), &responseErr); err != nil {
panic(err)
}
if responseErr.Error != "" {
panic("cannot start node: " + responseErr.Error)
}
}()
return waitForNodeStart
}
//nolint: deadcode
func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, APIDetailedResponse)) {
result := ValidateNodeConfig(C.CString(config))

View File

@ -10,10 +10,10 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/api"
"github.com/status-im/status-go/exportlogs"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/profiling"
"github.com/status-im/status-go/services/personal"
@ -28,6 +28,24 @@ var statusBackend = api.NewStatusBackend()
// All general log messages in this package should be routed through this logger.
var logger = log.New("package", "status-go/mobile")
// OpenAccounts opens database and returns accounts list.
func OpenAccounts(datadir string) string {
statusBackend.UpdateRootDataDir(datadir)
err := statusBackend.OpenAccounts()
if err != nil {
return makeJSONResponse(err)
}
accs, err := statusBackend.GetAccounts()
if err != nil {
return makeJSONResponse(err)
}
data, err := json.Marshal(accs)
if err != nil {
return makeJSONResponse(err)
}
return string(data)
}
// GenerateConfig for status node.
func GenerateConfig(datadir string, networkID int) string {
config, err := params.NewNodeConfig(datadir, uint64(networkID))
@ -43,28 +61,6 @@ func GenerateConfig(datadir string, networkID int) string {
return string(outBytes)
}
// StartNode starts the Ethereum Status node.
func StartNode(configJSON string) string {
config, err := params.NewConfigFromJSON(configJSON)
if err != nil {
return makeJSONResponse(err)
}
if err := logutils.OverrideRootLogWithConfig(config, false); err != nil {
return makeJSONResponse(err)
}
api.RunAsync(func() error { return statusBackend.StartNode(config) })
return makeJSONResponse(nil)
}
// StopNode stops the Ethereum Status node.
func StopNode() string {
api.RunAsync(statusBackend.StopNode)
return makeJSONResponse(nil)
}
// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature.
func ExtractGroupMembershipSignatures(signaturePairsStr string) string {
var signaturePairs [][2]string
@ -209,7 +205,6 @@ func CallPrivateRPC(inputJSON string) string {
// just modified to handle the function arg passing.
func CreateAccount(password string) string {
info, mnemonic, err := statusBackend.AccountManager().CreateAccount(password)
errString := ""
if err != nil {
fmt.Fprintln(os.Stderr, err)
@ -326,13 +321,58 @@ func VerifyAccountPassword(keyStoreDir, address, password string) string {
// Login loads a key file (for a given address), tries to decrypt it using the password,
// to verify ownership if verified, purges all the previous identities from Whisper,
// and injects verified key as shh identity.
func Login(loginParamsJSON string) string {
params, err := account.ParseLoginParams(loginParamsJSON)
func Login(accountData, password string) string {
var account multiaccounts.Account
err := json.Unmarshal([]byte(accountData), &account)
if err != nil {
return prepareJSONResponseWithCode(nil, err, codeFailedParseParams)
return makeJSONResponse(err)
}
api.RunAsync(func() error {
log.Debug("start a node with account", "address", account.Address)
err := statusBackend.StartNodeWithAccount(account, password)
if err != nil {
log.Error("failed to start a node", "address", account.Address, "error", err)
return err
}
log.Debug("started a node with", "address", account.Address)
return nil
})
return makeJSONResponse(nil)
}
err = statusBackend.SelectAccount(params)
// SaveAccountAndLogin saves account in status-go database..
func SaveAccountAndLogin(accountData, password, configJSON, subaccountData string) string {
var account multiaccounts.Account
err := json.Unmarshal([]byte(accountData), &account)
if err != nil {
return makeJSONResponse(err)
}
var conf params.NodeConfig
err = json.Unmarshal([]byte(configJSON), &conf)
if err != nil {
return makeJSONResponse(err)
}
var subaccs []accounts.Account
err = json.Unmarshal([]byte(subaccountData), &subaccs)
if err != nil {
return makeJSONResponse(err)
}
api.RunAsync(func() error {
log.Debug("starting a node, and saving account with configuration", "address", account.Address)
err := statusBackend.StartNodeWithAccountAndConfig(account, password, &conf, subaccs)
if err != nil {
log.Error("failed to start node and save account", "address", account.Address, "error", err)
return err
}
log.Debug("started a node, and saved account", "address", account.Address)
return nil
})
return makeJSONResponse(nil)
}
// InitKeystore initialize keystore before doing any operations with keys.
func InitKeystore(keydir string) string {
err := statusBackend.AccountManager().InitKeystore(keydir)
return makeJSONResponse(err)
}
@ -346,7 +386,11 @@ func LoginWithKeycard(chatKeyData, encryptionKeyData string) string {
// Logout is equivalent to clearing whisper identities.
func Logout() string {
err := statusBackend.Logout()
return makeJSONResponse(err)
if err != nil {
makeJSONResponse(err)
}
api.RunAsync(statusBackend.StopNode)
return makeJSONResponse(nil)
}
// SignMessage unmarshals rpc params {data, address, password} and

View File

@ -0,0 +1,168 @@
package accounts
import (
"database/sql"
"errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/multiaccounts/accounts/migrations"
"github.com/status-im/status-go/sqlite"
)
const (
uniqueChatConstraint = "UNIQUE constraint failed: accounts.chat"
uniqueWalletConstraint = "UNIQUE constraint failed: accounts.wallet"
)
var (
// ErrWalletNotUnique returned if another account has `wallet` field set to true.
ErrWalletNotUnique = errors.New("another account is set to be default wallet. disable it before using new")
// ErrChatNotUnique returned if another account has `chat` field set to true.
ErrChatNotUnique = errors.New("another account is set to be default chat. disable it before using new")
)
type Account struct {
Address common.Address `json:"address"`
Wallet bool `json:"wallet"`
Chat bool `json:"chat"`
Type string `json:"type"`
Storage string `json:"storage"`
Path string `json:"path"`
PublicKey hexutil.Bytes `json:"publicKey"`
Name string `json:"name"`
Color string `json:"color"`
}
// Database sql wrapper for operations with browser objects.
type Database struct {
db *sql.DB
}
// Close closes database.
func (db Database) Close() error {
return db.db.Close()
}
// InitializeDB creates db file at a given path and applies migrations.
func InitializeDB(path, password string) (*Database, error) {
db, err := sqlite.OpenDB(path, password)
if err != nil {
return nil, err
}
err = migrations.Migrate(db)
if err != nil {
return nil, err
}
return &Database{db: db}, nil
}
func (db *Database) SaveConfig(typ string, value interface{}) error {
_, err := db.db.Exec("INSERT OR REPLACE INTO settings (type, value) VALUES (?, ?)", typ, &sqlite.JSONBlob{value})
return err
}
func (db *Database) GetConfig(typ string, value interface{}) error {
return db.db.QueryRow("SELECT value FROM settings WHERE type = ?", typ).Scan(&sqlite.JSONBlob{value})
}
func (db *Database) GetBlob(typ string) (rst []byte, err error) {
return rst, db.db.QueryRow("SELECT value FROM settings WHERE type = ?", typ).Scan(&rst)
}
func (db *Database) GetAccounts() ([]Account, error) {
rows, err := db.db.Query("SELECT address, wallet, chat, type, storage, pubkey, path, name, color FROM accounts")
if err != nil {
return nil, err
}
accounts := []Account{}
pubkey := []byte{}
for rows.Next() {
acc := Account{}
err := rows.Scan(
&acc.Address, &acc.Wallet, &acc.Chat, &acc.Type, &acc.Storage,
&pubkey, &acc.Path, &acc.Name, &acc.Color)
if err != nil {
return nil, err
}
if lth := len(pubkey); lth > 0 {
acc.PublicKey = make(hexutil.Bytes, lth)
copy(acc.PublicKey, pubkey)
}
accounts = append(accounts, acc)
}
return accounts, nil
}
func (db *Database) SaveAccounts(accounts []Account) (err error) {
var (
tx *sql.Tx
insert *sql.Stmt
update *sql.Stmt
)
tx, err = db.db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
// NOTE(dshulyak) replace all record values using address (primary key)
// can't use `insert or replace` because of the additional constraints (wallet and chat)
insert, err = tx.Prepare("INSERT OR IGNORE INTO accounts (address) VALUES (?)")
if err != nil {
return err
}
update, err = tx.Prepare("UPDATE accounts SET wallet = ?, chat = ?, type = ?, storage = ?, pubkey = ?, path = ?, name = ?, color = ? WHERE address = ?")
if err != nil {
return err
}
for i := range accounts {
acc := &accounts[i]
_, err = insert.Exec(acc.Address)
if err != nil {
return
}
_, err = update.Exec(acc.Wallet, acc.Chat, acc.Type, acc.Storage, acc.PublicKey, acc.Path, acc.Name, acc.Color, acc.Address)
if err != nil {
switch err.Error() {
case uniqueChatConstraint:
err = ErrChatNotUnique
case uniqueWalletConstraint:
err = ErrWalletNotUnique
}
return
}
}
return
}
func (db *Database) GetWalletAddress() (rst common.Address, err error) {
err = db.db.QueryRow("SELECT address FROM accounts WHERE wallet = 1").Scan(&rst)
return
}
func (db *Database) GetChatAddress() (rst common.Address, err error) {
err = db.db.QueryRow("SELECT address FROM accounts WHERE chat = 1").Scan(&rst)
return
}
func (db *Database) GetAddresses() (rst []common.Address, err error) {
rows, err := db.db.Query("SELECT address FROM accounts")
if err != nil {
return nil, err
}
for rows.Next() {
addr := common.Address{}
err = rows.Scan(&addr)
if err != nil {
return nil, err
}
rst = append(rst, addr)
}
return rst, nil
}

View File

@ -0,0 +1,158 @@
package accounts
import (
"database/sql"
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/params"
"github.com/stretchr/testify/require"
)
func setupTestDB(t *testing.T) (*Database, func()) {
tmpfile, err := ioutil.TempFile("", "settings-tests-")
require.NoError(t, err)
db, err := InitializeDB(tmpfile.Name(), "settings-tests")
require.NoError(t, err)
return db, func() {
require.NoError(t, db.Close())
require.NoError(t, os.Remove(tmpfile.Name()))
}
}
func TestConfig(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
conf := params.NodeConfig{
NetworkID: 10,
DataDir: "test",
}
require.NoError(t, db.SaveConfig("node-config", conf))
var rst params.NodeConfig
require.NoError(t, db.GetConfig("node-config", &rst))
require.Equal(t, conf, rst)
}
func TestBlob(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
tag := "random-param"
param := 10
require.NoError(t, db.SaveConfig(tag, param))
expected, err := json.Marshal(param)
require.NoError(t, err)
rst, err := db.GetBlob(tag)
require.NoError(t, err)
require.Equal(t, expected, rst)
}
func TestSaveAccounts(t *testing.T) {
type testCase struct {
description string
accounts []Account
err error
}
for _, tc := range []testCase{
{
description: "NoError",
accounts: []Account{
{Address: common.Address{0x01}, Chat: true, Wallet: true},
{Address: common.Address{0x02}},
},
},
{
description: "UniqueChat",
accounts: []Account{
{Address: common.Address{0x01}, Chat: true},
{Address: common.Address{0x02}, Chat: true},
},
err: ErrChatNotUnique,
},
{
description: "UniqueWallet",
accounts: []Account{
{Address: common.Address{0x01}, Wallet: true},
{Address: common.Address{0x02}, Wallet: true},
},
err: ErrWalletNotUnique,
},
} {
t.Run(tc.description, func(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
require.Equal(t, tc.err, db.SaveAccounts(tc.accounts))
})
}
}
func TestUpdateAccounts(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
accounts := []Account{
{Address: common.Address{0x01}, Chat: true, Wallet: true},
{Address: common.Address{0x02}},
}
require.NoError(t, db.SaveAccounts(accounts))
accounts[0].Chat = false
accounts[1].Chat = true
require.NoError(t, db.SaveAccounts(accounts))
rst, err := db.GetAccounts()
require.NoError(t, err)
require.Equal(t, accounts, rst)
}
func TestGetAddresses(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
accounts := []Account{
{Address: common.Address{0x01}, Chat: true, Wallet: true},
{Address: common.Address{0x02}},
}
require.NoError(t, db.SaveAccounts(accounts))
addresses, err := db.GetAddresses()
require.NoError(t, err)
require.Equal(t, []common.Address{{0x01}, {0x02}}, addresses)
}
func TestGetWalletAddress(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
address := common.Address{0x01}
_, err := db.GetWalletAddress()
require.Equal(t, err, sql.ErrNoRows)
require.NoError(t, db.SaveAccounts([]Account{{Address: address, Wallet: true}}))
wallet, err := db.GetWalletAddress()
require.NoError(t, err)
require.Equal(t, address, wallet)
}
func TestGetChatAddress(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
address := common.Address{0x01}
_, err := db.GetChatAddress()
require.Equal(t, err, sql.ErrNoRows)
require.NoError(t, db.SaveAccounts([]Account{{Address: address, Chat: true}}))
chat, err := db.GetChatAddress()
require.NoError(t, err)
require.Equal(t, address, chat)
}
func TestGetAccounts(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
accounts := []Account{
{Address: common.Address{0x01}, Chat: true, Wallet: true},
{Address: common.Address{0x02}, PublicKey: hexutil.Bytes{0x01, 0x02}},
{Address: common.Address{0x03}, PublicKey: hexutil.Bytes{0x02, 0x03}},
}
require.NoError(t, db.SaveAccounts(accounts))
rst, err := db.GetAccounts()
require.NoError(t, err)
require.Equal(t, accounts, rst)
}

View File

@ -0,0 +1,6 @@
package accounts
const (
// NodeConfigTag tag for a node configuration.
NodeConfigTag = "node-config"
)

View File

@ -0,0 +1,127 @@
package migrations
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
func bindata_read(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
return buf.Bytes(), nil
}
var __0001_settings_down_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\x4e\x2d\x29\xc9\xcc\x4b\x2f\xb6\xe6\x42\x12\x4c\x4c\x4e\xce\x2f\xcd\x2b\x29\xb6\xe6\x02\x04\x00\x00\xff\xff\x2c\x0a\x12\xf1\x2a\x00\x00\x00")
func _0001_settings_down_sql() ([]byte, error) {
return bindata_read(
__0001_settings_down_sql,
"0001_settings.down.sql",
)
}
var __0001_settings_up_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x8f\xb1\x4e\xc3\x30\x10\x40\xf7\xfb\x8a\x1b\xa9\x94\x3f\xc8\xe4\xb4\x87\x62\x11\x6c\x70\x1d\x92\x4e\x95\x49\xad\xb6\x22\x24\x21\xb6\x41\xfd\x7b\x14\x97\x54\x1d\x4a\x37\x3f\xdf\xe9\xe9\xdd\x52\x11\xd3\x84\x9a\x65\x05\x21\x7f\x44\x21\x35\x52\xcd\xd7\x7a\x8d\xce\x7a\x7f\xec\xf6\x0e\x1f\xc0\x9f\x06\x8b\x6f\x4c\x2d\x73\xa6\xf0\x45\xf1\x67\xa6\x36\xf8\x44\x9b\x04\xbe\x4d\x1b\x2c\x66\x85\xcc\x60\x81\x15\xd7\xb9\x2c\x35\x2a\x59\xf1\x55\x0a\x70\x47\x6e\x9a\xa6\x0f\x9d\x9f\xe4\x66\xb7\x1b\xad\x73\xb7\xfd\x3f\xa6\x6d\xad\xc7\x4c\xca\x82\x98\x48\xa0\x39\x98\x2b\x8a\x5d\x9a\x6a\x9d\x80\xf3\xfd\x68\xf6\x33\x0d\xe1\xfd\xc3\x9e\x62\x57\x02\x83\xf1\x87\xbf\xff\xce\x7c\xce\x2b\x4d\xdf\xf6\x63\x7c\xff\x5f\x5e\x0a\xfe\x5a\x12\x72\xb1\xa2\x1a\x43\x77\xfc\x0a\x76\x7b\x2e\xda\xce\xd5\x52\x5c\xdd\x72\x9e\x2d\xb0\xca\x49\xd1\x05\xd3\x7b\xba\xe9\xa0\xdb\xb2\x69\x72\x51\x45\x48\xe1\x37\x00\x00\xff\xff\xfe\xcd\xfe\xc2\xaf\x01\x00\x00")
func _0001_settings_up_sql() ([]byte, error) {
return bindata_read(
__0001_settings_up_sql,
"0001_settings.up.sql",
)
}
var _doc_go = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00")
func doc_go() ([]byte, error) {
return bindata_read(
_doc_go,
"doc.go",
)
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
return f()
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
"0001_settings.down.sql": _0001_settings_down_sql,
"0001_settings.up.sql": _0001_settings_up_sql,
"doc.go": doc_go,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for name := range node.Children {
rv = append(rv, name)
}
return rv, nil
}
type _bintree_t struct {
Func func() ([]byte, error)
Children map[string]*_bintree_t
}
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"0001_settings.down.sql": &_bintree_t{_0001_settings_down_sql, map[string]*_bintree_t{
}},
"0001_settings.up.sql": &_bintree_t{_0001_settings_up_sql, map[string]*_bintree_t{
}},
"doc.go": &_bintree_t{doc_go, map[string]*_bintree_t{
}},
}}

View File

@ -0,0 +1,18 @@
package migrations
import (
"database/sql"
bindata "github.com/status-im/migrate/v4/source/go_bindata"
"github.com/status-im/status-go/sqlite"
)
// Migrate applies migrations.
func Migrate(db *sql.DB) error {
return sqlite.Migrate(db, bindata.Resource(
AssetNames(),
func(name string) ([]byte, error) {
return Asset(name)
},
))
}

View File

@ -0,0 +1,2 @@
DROP TABLE settings;
DROP TABLE accounts;

View File

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS settings (
type VARCHAR PRIMARY KEY,
value BLOB
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS accounts (
address VARCHAR PRIMARY KEY,
wallet BOOLEAN,
chat BOOLEAN,
type TEXT,
storage TEXT,
pubkey BLOB,
path TEXT,
name TEXT,
color TEXT
) WITHOUT ROWID;
CREATE UNIQUE INDEX unique_wallet_address ON accounts (wallet) WHERE (wallet);
CREATE UNIQUE INDEX unique_chat_address ON accounts (chat) WHERE (chat);

View File

@ -0,0 +1,3 @@
package sql
//go:generate go-bindata -pkg migrations -o ../bindata.go ./

77
multiaccounts/database.go Normal file
View File

@ -0,0 +1,77 @@
package multiaccounts
import (
"database/sql"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/multiaccounts/migrations"
"github.com/status-im/status-go/sqlite"
)
// Account stores public information about account.
type Account struct {
Name string `json:"name"`
Address common.Address `json:"address"`
Timestamp int64 `json:"timestamp"`
PhotoPath string `json:"photo-path"`
}
// InitializeDB creates db file at a given path and applies migrations.
func InitializeDB(path string) (*Database, error) {
db, err := sqlite.OpenUnecryptedDB(path)
if err != nil {
return nil, err
}
err = migrations.Migrate(db)
if err != nil {
return nil, err
}
return &Database{db: db}, nil
}
type Database struct {
db *sql.DB
}
func (db *Database) Close() error {
return db.db.Close()
}
func (db *Database) GetAccounts() ([]Account, error) {
rows, err := db.db.Query("SELECT address, name, loginTimestamp, photoPath from accounts ORDER BY loginTimestamp DESC")
if err != nil {
return nil, err
}
rst := []Account{}
inthelper := sql.NullInt64{}
for rows.Next() {
acc := Account{}
err = rows.Scan(&acc.Address, &acc.Name, &inthelper, &acc.PhotoPath)
if err != nil {
return nil, err
}
acc.Timestamp = inthelper.Int64
rst = append(rst, acc)
}
return rst, nil
}
func (db *Database) SaveAccount(account Account) error {
_, err := db.db.Exec("INSERT OR REPLACE INTO accounts (address, name, photoPath) VALUES (?, ?, ?)", account.Address, account.Name, account.PhotoPath)
return err
}
func (db *Database) UpdateAccount(account Account) error {
_, err := db.db.Exec("UPDATE accounts SET name = ?, photoPath = ? WHERE address = ?", account.Name, account.PhotoPath, account.Address)
return err
}
func (db *Database) UpdateAccountTimestamp(address common.Address, loginTimestamp int64) error {
_, err := db.db.Exec("UPDATE accounts SET loginTimestamp = ? WHERE address = ?", loginTimestamp, address)
return err
}
func (db *Database) DeleteAccount(address common.Address) error {
_, err := db.db.Exec("DELETE FROM accounts WHERE address = ?", address)
return err
}

View File

@ -0,0 +1,62 @@
package multiaccounts
import (
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func setupTestDB(t *testing.T) (*Database, func()) {
tmpfile, err := ioutil.TempFile("", "accounts-tests-")
require.NoError(t, err)
db, err := InitializeDB(tmpfile.Name())
require.NoError(t, err)
return db, func() {
require.NoError(t, db.Close())
require.NoError(t, os.Remove(tmpfile.Name()))
}
}
func TestAccounts(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
expected := Account{Name: "string", Address: common.Address{0xff}}
require.NoError(t, db.SaveAccount(expected))
accounts, err := db.GetAccounts()
require.NoError(t, err)
require.Len(t, accounts, 1)
require.Equal(t, expected, accounts[0])
}
func TestAccountsUpdate(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
expected := Account{Address: common.Address{0x01}}
require.NoError(t, db.SaveAccount(expected))
expected.PhotoPath = "chars"
require.NoError(t, db.UpdateAccount(expected))
rst, err := db.GetAccounts()
require.NoError(t, err)
require.Len(t, rst, 1)
require.Equal(t, expected, rst[0])
}
func TestLoginUpdate(t *testing.T) {
db, stop := setupTestDB(t)
defer stop()
accounts := []Account{{Name: "first", Address: common.Address{0xff}}, {Name: "second", Address: common.Address{0xf1}}}
for _, acc := range accounts {
require.NoError(t, db.SaveAccount(acc))
}
require.NoError(t, db.UpdateAccountTimestamp(accounts[0].Address, 100))
require.NoError(t, db.UpdateAccountTimestamp(accounts[1].Address, 10))
accounts[0].Timestamp = 100
accounts[1].Timestamp = 10
rst, err := db.GetAccounts()
require.NoError(t, err)
require.Equal(t, accounts, rst)
}

View File

@ -0,0 +1,127 @@
package migrations
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
func bindata_read(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
return buf.Bytes(), nil
}
var __0001_accounts_down_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x48\x4c\x4e\xce\x2f\xcd\x2b\x29\xb6\xe6\x02\x04\x00\x00\xff\xff\x96\x1e\x13\xa1\x15\x00\x00\x00")
func _0001_accounts_down_sql() ([]byte, error) {
return bindata_read(
__0001_accounts_down_sql,
"0001_accounts.down.sql",
)
}
var __0001_accounts_up_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x1c\xcb\x41\x0e\x82\x30\x10\x05\xd0\x7d\x4f\xf1\x97\x9a\x70\x03\x57\x05\xab\x4c\x44\x20\xc3\x20\xb0\x6c\x80\x08\x89\xb4\xc4\xd6\xfb\x9b\x70\x80\x97\xb1\xd1\x62\x20\x3a\x2d\x0c\xe8\x86\xb2\x12\x98\x9e\x1a\x69\x60\xc7\xd1\xff\x5c\x0c\x38\x29\x3b\x4d\xdf\x39\x04\xbc\x34\x67\xb9\x66\xd4\x4c\x4f\xcd\x03\x1e\x66\x48\x94\xb3\xdb\x0c\x31\xbd\x1c\xb8\x6c\x8b\x22\x51\x1f\xff\x5e\x9d\xac\xdb\x1c\xa2\xdd\x76\xa4\x74\x07\x95\x92\xa8\x7d\xf1\xd1\xd7\x36\x2e\x07\x50\x67\x74\x24\x79\xd5\x0a\xb8\xea\xe8\x7a\x51\xff\x00\x00\x00\xff\xff\x14\xa8\x25\xe1\x8f\x00\x00\x00")
func _0001_accounts_up_sql() ([]byte, error) {
return bindata_read(
__0001_accounts_up_sql,
"0001_accounts.up.sql",
)
}
var _doc_go = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00")
func doc_go() ([]byte, error) {
return bindata_read(
_doc_go,
"doc.go",
)
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
return f()
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
"0001_accounts.down.sql": _0001_accounts_down_sql,
"0001_accounts.up.sql": _0001_accounts_up_sql,
"doc.go": doc_go,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for name := range node.Children {
rv = append(rv, name)
}
return rv, nil
}
type _bintree_t struct {
Func func() ([]byte, error)
Children map[string]*_bintree_t
}
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"0001_accounts.down.sql": &_bintree_t{_0001_accounts_down_sql, map[string]*_bintree_t{
}},
"0001_accounts.up.sql": &_bintree_t{_0001_accounts_up_sql, map[string]*_bintree_t{
}},
"doc.go": &_bintree_t{doc_go, map[string]*_bintree_t{
}},
}}

View File

@ -0,0 +1,18 @@
package migrations
import (
"database/sql"
bindata "github.com/status-im/migrate/v4/source/go_bindata"
"github.com/status-im/status-go/sqlite"
)
// Migrate applies migrations.
func Migrate(db *sql.DB) error {
return sqlite.Migrate(db, bindata.Resource(
AssetNames(),
func(name string) ([]byte, error) {
return Asset(name)
},
))
}

View File

@ -0,0 +1 @@
DROP TABLE accounts;

View File

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS accounts (
address VARCHAR PRIMARY KEY,
name TEXT NOT NULL,
loginTimestamp BIG INT,
photoPath TEXT
) WITHOUT ROWID;

View File

@ -0,0 +1,3 @@
package sql
//go:generate go-bindata -pkg migrations -o ../bindata.go ./

View File

@ -9,6 +9,7 @@ import (
"path/filepath"
"time"
"github.com/ethereum/go-ethereum/accounts"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
@ -57,7 +58,7 @@ var (
var logger = log.New("package", "status-go/node")
// MakeNode creates a geth node entity
func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
func MakeNode(config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB) (*node.Node, error) {
// If DataDir is empty, it means we want to create an ephemeral node
// keeping data only in memory.
if config.DataDir != "" {
@ -82,17 +83,17 @@ func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
return nil, fmt.Errorf(ErrNodeMakeFailureFormat, err.Error())
}
err = activateServices(stack, config, db)
err = activateServices(stack, config, accs, db)
if err != nil {
return nil, err
}
return stack, nil
}
func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.DB) error {
func activateServices(stack *node.Node, config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB) error {
// start Ethereum service if we are not expected to use an upstream server
if !config.UpstreamConfig.Enabled {
if err := activateLightEthService(stack, config); err != nil {
if err := activateLightEthService(stack, accs, config); err != nil {
return fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err)
}
} else {
@ -107,7 +108,7 @@ func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.D
// Usually, they are provided by an ETH or a LES service, but when using
// upstream, we don't start any of these, so we need to start our own
// implementation.
if err := activatePersonalService(stack, config); err != nil {
if err := activatePersonalService(stack, accs, config); err != nil {
return fmt.Errorf("%v: %v", ErrPersonalServiceRegistrationFailure, err)
}
}
@ -248,7 +249,7 @@ func defaultStatusChainGenesisBlock() (*core.Genesis, error) {
}
// activateLightEthService configures and registers the eth.Ethereum service with a given node.
func activateLightEthService(stack *node.Node, config *params.NodeConfig) error {
func activateLightEthService(stack *node.Node, accs *accounts.Manager, config *params.NodeConfig) error {
if !config.LightEthConfig.Enabled {
logger.Info("LES protocol is disabled")
return nil
@ -269,13 +270,18 @@ func activateLightEthService(stack *node.Node, config *params.NodeConfig) error
MinTrustedFraction: config.LightEthConfig.MinTrustedFraction,
}
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return les.New(ctx, &ethConf)
// NOTE(dshulyak) here we set our instance of the accounts manager.
// without sharing same instance selected account won't be visible for personal_* methods.
nctx := &node.ServiceContext{}
*nctx = *ctx
nctx.AccountManager = accs
return les.New(nctx, &ethConf)
})
}
func activatePersonalService(stack *node.Node, config *params.NodeConfig) error {
func activatePersonalService(stack *node.Node, accs *accounts.Manager, config *params.NodeConfig) error {
return stack.Register(func(*node.ServiceContext) (node.Service, error) {
svc := personal.New(stack.AccountManager())
svc := personal.New(accs)
return svc, nil
})
}

View File

@ -3,6 +3,7 @@ package node
import (
"testing"
"github.com/ethereum/go-ethereum/accounts"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/status-im/status-go/params"
@ -17,7 +18,7 @@ func TestWhisperLightModeEnabledSetsEmptyBloomFilter(t *testing.T) {
},
}
node := New()
require.NoError(t, node.Start(&config))
require.NoError(t, node.Start(&config, &accounts.Manager{}))
defer func() {
require.NoError(t, node.Stop())
}()
@ -39,7 +40,7 @@ func TestWhisperLightModeEnabledSetsNilBloomFilter(t *testing.T) {
},
}
node := New()
require.NoError(t, node.Start(&config))
require.NoError(t, node.Start(&config, &accounts.Manager{}))
defer func() {
require.NoError(t, node.Stop())
}()

View File

@ -4,6 +4,7 @@ import (
"net"
"testing"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/params"
@ -23,7 +24,7 @@ func TestMakeNodeDefaultConfig(t *testing.T) {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
require.NoError(t, err)
_, err = MakeNode(config, db)
_, err = MakeNode(config, &accounts.Manager{}, db)
require.NoError(t, err)
}
@ -40,7 +41,7 @@ func TestMakeNodeWellFormedBootnodes(t *testing.T) {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
require.NoError(t, err)
_, err = MakeNode(config, db)
_, err = MakeNode(config, &accounts.Manager{}, db)
require.NoError(t, err)
}
@ -58,7 +59,7 @@ func TestMakeNodeMalformedBootnodes(t *testing.T) {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
require.NoError(t, err)
_, err = MakeNode(config, db)
_, err = MakeNode(config, &accounts.Manager{}, db)
require.NoError(t, err)
}

View File

@ -13,7 +13,6 @@ import (
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/les"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
@ -105,17 +104,19 @@ func (n *StatusNode) Server() *p2p.Server {
// Start starts current StatusNode, failing if it's already started.
// It accepts a list of services that should be added to the node.
func (n *StatusNode) Start(config *params.NodeConfig, services ...node.ServiceConstructor) error {
func (n *StatusNode) Start(config *params.NodeConfig, accs *accounts.Manager, services ...node.ServiceConstructor) error {
return n.StartWithOptions(config, StartOptions{
Services: services,
StartDiscovery: true,
Services: services,
StartDiscovery: true,
AccountsManager: accs,
})
}
// StartOptions allows to control some parameters of Start() method.
type StartOptions struct {
Services []node.ServiceConstructor
StartDiscovery bool
Services []node.ServiceConstructor
StartDiscovery bool
AccountsManager *accounts.Manager
}
// StartWithOptions starts current StatusNode, failing if it's already started.
@ -133,12 +134,12 @@ func (n *StatusNode) StartWithOptions(config *params.NodeConfig, options StartOp
db, err := db.Create(config.DataDir, params.StatusDatabase)
if err != nil {
return err
return fmt.Errorf("failed to create database at %s: %v", config.DataDir, err)
}
n.db = db
err = n.startWithDB(config, db, options.Services)
err = n.startWithDB(config, options.AccountsManager, db, options.Services)
// continue only if there was no error when starting node with a db
if err == nil && options.StartDiscovery && n.discoveryEnabled() {
@ -156,8 +157,8 @@ func (n *StatusNode) StartWithOptions(config *params.NodeConfig, options StartOp
return nil
}
func (n *StatusNode) startWithDB(config *params.NodeConfig, db *leveldb.DB, services []node.ServiceConstructor) error {
if err := n.createNode(config, db); err != nil {
func (n *StatusNode) startWithDB(config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB, services []node.ServiceConstructor) error {
if err := n.createNode(config, accs, db); err != nil {
return err
}
n.config = config
@ -173,8 +174,8 @@ func (n *StatusNode) startWithDB(config *params.NodeConfig, db *leveldb.DB, serv
return nil
}
func (n *StatusNode) createNode(config *params.NodeConfig, db *leveldb.DB) (err error) {
n.gethNode, err = MakeNode(config, db)
func (n *StatusNode) createNode(config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB) (err error) {
n.gethNode, err = MakeNode(config, accs, db)
return err
}
@ -638,29 +639,6 @@ func (n *StatusNode) AccountManager() (*accounts.Manager, error) {
return n.gethNode.AccountManager(), nil
}
// AccountKeyStore exposes reference to accounts key store
func (n *StatusNode) AccountKeyStore() (*keystore.KeyStore, error) {
n.mu.RLock()
defer n.mu.RUnlock()
if n.gethNode == nil {
return nil, ErrNoGethNode
}
accountManager := n.gethNode.AccountManager()
backends := accountManager.Backends(keystore.KeyStoreType)
if len(backends) == 0 {
return nil, ErrAccountKeyStoreMissing
}
keyStore, ok := backends[0].(*keystore.KeyStore)
if !ok {
return nil, ErrAccountKeyStoreMissing
}
return keyStore, nil
}
// RPCClient exposes reference to RPC client connected to the running node.
func (n *StatusNode) RPCClient() *rpc.Client {
n.mu.RLock()

View File

@ -58,7 +58,7 @@ func createAndStartStatusNode(config *params.NodeConfig) (*node.StatusNode, erro
},
}
statusNode := node.New()
return statusNode, statusNode.Start(config, services...)
return statusNode, statusNode.Start(config, nil, services...)
}
func TestNodeRPCClientCallOnlyPublicAPIs(t *testing.T) {

View File

@ -36,13 +36,8 @@ func TestStatusNodeStart(t *testing.T) {
require.Nil(t, n.Config())
require.Nil(t, n.RPCClient())
require.Equal(t, 0, n.PeerCount())
_, err = n.AccountManager()
require.EqualError(t, err, ErrNoGethNode.Error())
_, err = n.AccountKeyStore()
require.EqualError(t, err, ErrNoGethNode.Error())
// start node
require.NoError(t, n.Start(config))
require.NoError(t, n.Start(config, nil))
// checks after node is started
require.True(t, n.IsRunning())
@ -53,11 +48,8 @@ func TestStatusNodeStart(t *testing.T) {
accountManager, err := n.AccountManager()
require.Nil(t, err)
require.NotNil(t, accountManager)
keyStore, err := n.AccountKeyStore()
require.Nil(t, err)
require.NotNil(t, keyStore)
// try to start already started node
require.EqualError(t, n.Start(config), ErrNodeRunning.Error())
require.EqualError(t, n.Start(config, nil), ErrNodeRunning.Error())
// stop node
require.NoError(t, n.Stop())
@ -68,10 +60,6 @@ func TestStatusNodeStart(t *testing.T) {
require.Nil(t, n.GethNode())
require.Nil(t, n.RPCClient())
require.Equal(t, 0, n.PeerCount())
_, err = n.AccountManager()
require.EqualError(t, err, ErrNoGethNode.Error())
_, err = n.AccountKeyStore()
require.EqualError(t, err, ErrNoGethNode.Error())
}
func TestStatusNodeWithDataDir(t *testing.T) {
@ -94,7 +82,7 @@ func TestStatusNodeWithDataDir(t *testing.T) {
}
n := New()
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
require.NoError(t, n.Stop())
}
@ -140,7 +128,7 @@ func TestStatusNodeServiceGetters(t *testing.T) {
require.Nil(t, instance)
// start node
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
// checks after node is started
instance, err = service.getter()
@ -184,7 +172,7 @@ func TestStatusNodeAddPeer(t *testing.T) {
config := params.NodeConfig{
MaxPeers: math.MaxInt32,
}
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
defer func() { require.NoError(t, n.Stop()) }()
errCh := helpers.WaitForPeerAsync(n.Server(), peerURL, p2p.PeerEventTypeAdd, time.Second*5)
@ -226,7 +214,7 @@ func TestStatusNodeReconnectStaticPeers(t *testing.T) {
StaticNodes: []string{peerURL},
},
}
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
defer func() { require.NoError(t, n.Stop()) }()
// checks after node is started
@ -286,7 +274,7 @@ func TestStatusNodeRendezvousDiscovery(t *testing.T) {
AdvertiseAddr: "127.0.0.1",
}
n := New()
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
require.NotNil(t, n.discovery)
require.True(t, n.discovery.Running())
require.IsType(t, &discovery.Rendezvous{}, n.discovery)
@ -320,7 +308,7 @@ func TestStatusNodeDiscoverNode(t *testing.T) {
ListenAddr: "127.0.0.1:0",
}
n := New()
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
node, err := n.discoverNode()
require.NoError(t, err)
require.Equal(t, net.ParseIP("127.0.0.1").To4(), node.IP())
@ -331,7 +319,7 @@ func TestStatusNodeDiscoverNode(t *testing.T) {
ListenAddr: "127.0.0.1:0",
}
n = New()
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
node, err = n.discoverNode()
require.NoError(t, err)
require.Equal(t, net.ParseIP("127.0.0.2").To4(), node.IP())
@ -357,7 +345,7 @@ func TestChaosModeCheckRPCClientsUpstreamURL(t *testing.T) {
},
}
n := New()
require.NoError(t, n.Start(&config))
require.NoError(t, n.Start(&config, nil))
defer func() { require.NoError(t, n.Stop()) }()
require.NotNil(t, n.RPCClient())

View File

@ -0,0 +1,38 @@
Settings service
================
Settings service provides private API for storing all configuration for a selected account.
To enable:
1. Client must ensure that settings db is initialized in the api.Backend.
2. Add `settings` to APIModules in config.
API
---
### settings_saveConfig
#### Parameters
- `type`: `string` - configuratin type. if not unique error is raised.
- `conf`: `bytes` - raw json.
### settings_getConfig
#### Parameters
- `type`: string
#### Returns
- `conf` raw json
### settings_saveNodeConfig
Special case of the settings_saveConfig. In status-go we are using constant `node-config` as a type for node configuration.
Application depends on this value and will try to load it when node is started. This method is provided
in order to remove syncing mentioned constant between status-go and users.
#### Parameters
- `conf`: params.NodeConfig

View File

@ -0,0 +1,24 @@
package accounts
import (
"context"
"github.com/status-im/status-go/multiaccounts/accounts"
)
func NewAccountsAPI(db *accounts.Database) *API {
return &API{db}
}
// API is class with methods available over RPC.
type API struct {
db *accounts.Database
}
func (api *API) SaveAccounts(ctx context.Context, accounts []accounts.Account) error {
return api.db.SaveAccounts(accounts)
}
func (api *API) GetAccounts(ctx context.Context) ([]accounts.Account, error) {
return api.db.GetAccounts()
}

View File

@ -0,0 +1,34 @@
package accounts
import (
"errors"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/multiaccounts"
)
var (
// ErrUpdatingWrongAccount raised if caller tries to update any other account except one used for login.
ErrUpdatingWrongAccount = errors.New("failed to updating wrong account. please login with that account first")
)
func NewMultiAccountsAPI(db *multiaccounts.Database, manager *account.Manager) *MultiAccountsAPI {
return &MultiAccountsAPI{db: db, manager: manager}
}
// MultiAccountsAPI is class with methods available over RPC.
type MultiAccountsAPI struct {
db *multiaccounts.Database
manager *account.Manager
}
func (api *MultiAccountsAPI) UpdateAccount(account multiaccounts.Account) error {
expected, err := api.manager.MainAccountAddress()
if err != nil {
return err
}
if account.Address != expected {
return ErrUpdatingWrongAccount
}
return api.db.UpdateAccount(account)
}

View File

@ -0,0 +1,57 @@
package accounts
import (
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
)
// NewService initializes service instance.
func NewService(db *accounts.Database, mdb *multiaccounts.Database, manager *account.Manager) *Service {
return &Service{db, mdb, manager}
}
// Service is a browsers service.
type Service struct {
db *accounts.Database
mdb *multiaccounts.Database
manager *account.Manager
}
// Start a service.
func (s *Service) Start(*p2p.Server) error {
return nil
}
// Stop a service.
func (s *Service) Stop() error {
return nil
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "settings",
Version: "0.1.0",
Service: NewSettingsAPI(s.db),
},
{
Namespace: "accounts",
Version: "0.1.0",
Service: NewAccountsAPI(s.db),
},
{
Namespace: "multiaccounts",
Version: "0.1.0",
Service: NewMultiAccountsAPI(s.mdb, s.manager),
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}

View File

@ -0,0 +1,34 @@
package accounts
import (
"context"
"encoding/json"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
)
func NewSettingsAPI(db *accounts.Database) *SettingsAPI {
return &SettingsAPI{db}
}
// SettingsAPI is class with methods available over RPC.
type SettingsAPI struct {
db *accounts.Database
}
func (api *SettingsAPI) SaveConfig(ctx context.Context, typ string, conf json.RawMessage) error {
return api.db.SaveConfig(typ, conf)
}
func (api *SettingsAPI) GetConfig(ctx context.Context, typ string) (json.RawMessage, error) {
rst, err := api.db.GetBlob(typ)
if err != nil {
return nil, err
}
return json.RawMessage(rst), nil
}
func (api *SettingsAPI) SaveNodeConfig(ctx context.Context, conf *params.NodeConfig) error {
return api.db.SaveConfig(accounts.NodeConfigTag, conf)
}

View File

@ -16,6 +16,9 @@ const (
// EventChainDataRemoved is triggered when node's chain data is removed
EventChainDataRemoved = "chaindata.removed"
// EventLoggedIn is once node was injected with user account and ready to be used.
EventLoggedIn = "node.login"
)
// NodeCrashEvent is special kind of error, used to report node crashes
@ -53,3 +56,7 @@ func SendNodeStopped() {
func SendChainDataRemoved() {
send(EventChainDataRemoved, nil)
}
func SendLoggedIn() {
send(EventLoggedIn, nil)
}

39
sqlite/fields.go Normal file
View File

@ -0,0 +1,39 @@
package sqlite
import (
"database/sql/driver"
"encoding/json"
"errors"
"reflect"
)
// JSONBlob type for marshaling/unmarshaling inner type to json.
type JSONBlob struct {
Data interface{}
}
// Scan implements interface.
func (blob *JSONBlob) Scan(value interface{}) error {
dataVal := reflect.ValueOf(blob.Data)
if value == nil || dataVal.Kind() == reflect.Ptr && dataVal.IsNil() {
return nil
}
bytes, ok := value.([]byte)
if !ok {
return errors.New("not a byte slice")
}
if len(bytes) == 0 {
return nil
}
err := json.Unmarshal(bytes, blob.Data)
return err
}
// Value implements interface.
func (blob *JSONBlob) Value() (driver.Value, error) {
dataVal := reflect.ValueOf(blob.Data)
if blob.Data == nil || dataVal.Kind() == reflect.Ptr && dataVal.IsNil() {
return nil, nil
}
return json.Marshal(blob.Data)
}

View File

@ -8,11 +8,15 @@ import (
_ "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation
)
// The reduced number of kdf iterations (for performance reasons) which is
// currently used for derivation of the database key
// https://github.com/status-im/status-go/pull/1343
// https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA
const kdfIterationsNumber = 3200
const (
// The reduced number of kdf iterations (for performance reasons) which is
// currently used for derivation of the database key
// https://github.com/status-im/status-go/pull/1343
// https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA
kdfIterationsNumber = 3200
// WALMode for sqlite.
WALMode = "wal"
)
func openDB(path, key string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path)
@ -43,7 +47,7 @@ func openDB(path, key string) (*sql.DB, error) {
if err != nil {
return nil, err
}
if mode != "wal" {
if mode != WALMode {
return nil, fmt.Errorf("unable to set journal_mode to WAL. actual mode %s", mode)
}
@ -54,3 +58,31 @@ func openDB(path, key string) (*sql.DB, error) {
func OpenDB(path, key string) (*sql.DB, error) {
return openDB(path, key)
}
// OpenUnecryptedDB opens database with setting PRAGMA key.
func OpenUnecryptedDB(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, err
}
// Disable concurrent access as not supported by the driver
db.SetMaxOpenConns(1)
if _, err = db.Exec("PRAGMA foreign_keys=ON"); err != nil {
return nil, err
}
// readers do not block writers and faster i/o operations
// https://www.sqlite.org/draft/wal.html
// must be set after db is encrypted
var mode string
err = db.QueryRow("PRAGMA journal_mode=WAL").Scan(&mode)
if err != nil {
return nil, err
}
if mode != WALMode {
return nil, fmt.Errorf("unable to set journal_mode to WAL. actual mode %s", mode)
}
return db, nil
}

View File

@ -55,6 +55,7 @@ func (s *DevNodeSuite) SetupTest() {
config.WalletConfig.Enabled = true
config.UpstreamConfig.URL = s.miner.IPCEndpoint()
s.backend = api.NewStatusBackend()
s.Require().NoError(s.backend.AccountManager().InitKeystore(config.KeyStoreDir))
s.Require().NoError(s.backend.StartNode(config))
s.Remote, err = s.miner.Attach()
s.Require().NoError(err)

View File

@ -67,8 +67,7 @@ func (s *AccountsTestSuite) TestImportSingleExtendedKey() {
s.StartTestBackend()
defer s.StopTestBackend()
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
s.NoError(err)
keyStore := s.Backend.AccountManager().GetKeystore()
s.NotNil(keyStore)
// create a master extended key
@ -95,8 +94,7 @@ func (s *AccountsTestSuite) TestImportAccount() {
s.StartTestBackend()
defer s.StopTestBackend()
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
s.NoError(err)
keyStore := s.Backend.AccountManager().GetKeystore()
s.NotNil(keyStore)
// create a private key
@ -119,8 +117,8 @@ func (s *AccountsTestSuite) TestRecoverAccount() {
s.StartTestBackend()
defer s.StopTestBackend()
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
s.NoError(err)
keyStore := s.Backend.AccountManager().GetKeystore()
s.NotNil(keyStore)
// create an acc
accountInfo, mnemonic, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)

View File

@ -90,6 +90,7 @@ func (s *APITestSuite) TestRaceConditions() {
if rnd.Intn(100) > 75 { // introduce random delays
time.Sleep(500 * time.Millisecond)
}
s.NoError(s.backend.AccountManager().InitKeystore(randConfig.KeyStoreDir))
go randFunc(randConfig)
}
@ -124,6 +125,7 @@ func (s *APITestSuite) TestEventsNodeStartStop() {
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
s.NoError(err)
s.NoError(s.backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
s.Require().NoError(s.backend.StartNode(nodeConfig))
s.NoError(s.backend.StopNode())
s.verifyEnvelopes(envelopes, signal.EventNodeStarted, signal.EventNodeReady, signal.EventNodeStopped)
@ -168,12 +170,13 @@ func (s *APITestSuite) TestNodeStartCrash() {
defer func() { s.NoError(db.Close()) }()
// start node outside the manager (on the same port), so that manager node.Start() method fails
outsideNode, err := node.MakeNode(nodeConfig, db)
outsideNode, err := node.MakeNode(nodeConfig, nil, db)
s.NoError(err)
err = outsideNode.Start()
s.NoError(err)
// now try starting using node manager, it should fail (error is irrelevant as it is implementation detail)
s.NoError(s.backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
s.Error(<-api.RunAsync(func() error { return s.backend.StartNode(nodeConfig) }))
select {

View File

@ -25,7 +25,7 @@ func (s *APIBackendTestSuite) TestNetworkSwitching() {
// Get test node configuration.
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
s.NoError(err)
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
s.False(s.Backend.IsNodeRunning())
s.Require().NoError(s.Backend.StartNode(nodeConfig))
s.True(s.Backend.IsNodeRunning())
@ -87,6 +87,7 @@ func (s *APIBackendTestSuite) TestRestartNode() {
// get config
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
s.NoError(err)
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
s.False(s.Backend.IsNodeRunning())
require.NoError(s.Backend.StartNode(nodeConfig))

View File

@ -5,7 +5,6 @@ import (
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/les"
gethnode "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
@ -71,20 +70,6 @@ func (s *ManagerTestSuite) TestReferencesWithoutStartedNode() {
},
node.ErrNoRunningNode,
},
{
"non-null manager, no running node, get AccountManager",
func() (interface{}, error) {
return s.StatusNode.AccountManager()
},
node.ErrNoGethNode,
},
{
"non-null manager, no running node, get AccountKeyStore",
func() (interface{}, error) {
return s.StatusNode.AccountKeyStore()
},
node.ErrNoGethNode,
},
{
"non-null manager, no running node, get RPC Client",
func() (interface{}, error) {
@ -148,13 +133,6 @@ func (s *ManagerTestSuite) TestReferencesWithStartedNode() {
},
&accounts.Manager{},
},
{
"node is running, get AccountKeyStore",
func() (interface{}, error) {
return s.StatusNode.AccountKeyStore()
},
&keystore.KeyStore{},
},
{
"node is running, get RPC Client",
func() (interface{}, error) {
@ -183,12 +161,12 @@ func (s *ManagerTestSuite) TestNodeStartStop() {
// start node
s.False(s.StatusNode.IsRunning())
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
// wait till node is started
s.True(s.StatusNode.IsRunning())
// try starting another node (w/o stopping the previously started node)
s.Equal(node.ErrNodeRunning, s.StatusNode.Start(nodeConfig))
s.Equal(node.ErrNodeRunning, s.StatusNode.Start(nodeConfig, nil))
// now stop node
time.Sleep(100 * time.Millisecond) //https://github.com/status-im/status-go/issues/429#issuecomment-339663163
@ -196,7 +174,7 @@ func (s *ManagerTestSuite) TestNodeStartStop() {
s.False(s.StatusNode.IsRunning())
// start new node with exactly the same config
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
s.True(s.StatusNode.IsRunning())
// finally stop the node
@ -209,7 +187,7 @@ func (s *ManagerTestSuite) TestNetworkSwitching() {
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
s.NoError(err)
s.False(s.StatusNode.IsRunning())
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
// wait till node is started
s.Require().True(s.StatusNode.IsRunning())
@ -225,7 +203,7 @@ func (s *ManagerTestSuite) TestNetworkSwitching() {
// start new node with completely different config
nodeConfig, err = MakeTestNodeConfig(params.RinkebyNetworkID)
s.NoError(err)
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
s.True(s.StatusNode.IsRunning())
// make sure we are on another network indeed
@ -251,7 +229,7 @@ func (s *ManagerTestSuite) TestStartWithUpstreamEnabled() {
nodeConfig.UpstreamConfig.Enabled = true
nodeConfig.UpstreamConfig.URL = networkURL
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
s.True(s.StatusNode.IsRunning())
time.Sleep(100 * time.Millisecond) //https://github.com/status-im/status-go/issues/429#issuecomment-339663163

View File

@ -49,7 +49,7 @@ func (s *RPCTestSuite) TestCallRPC() {
nodeConfig.UpstreamConfig.URL = networkURL
}
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
rpcClient := s.StatusNode.RPCClient()
s.NotNil(rpcClient)
@ -127,7 +127,7 @@ func (s *RPCTestSuite) TestCallRawResult() {
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
s.NoError(err)
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
client := s.StatusNode.RPCPrivateClient()
s.NotNil(client)
@ -145,7 +145,7 @@ func (s *RPCTestSuite) TestCallRawResultGetTransactionReceipt() {
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
s.NoError(err)
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
client := s.StatusNode.RPCClient()
s.NotNil(client)

View File

@ -73,6 +73,7 @@ func (s *BaseJSONRPCSuite) SetupTest(upstreamEnabled, statusServiceEnabled, debu
nodeConfig, err := utils.MakeTestNodeConfig(utils.GetNetworkID())
s.NoError(err)
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
nodeConfig.IPCEnabled = false
nodeConfig.EnableStatusService = statusServiceEnabled

View File

@ -51,7 +51,7 @@ func (s *StatusNodeTestSuite) StartTestNode(opts ...TestNodeOption) {
s.NoError(importTestAccounts(nodeConfig.KeyStoreDir))
s.False(s.StatusNode.IsRunning())
s.NoError(s.StatusNode.Start(nodeConfig))
s.NoError(s.StatusNode.Start(nodeConfig, nil))
s.True(s.StatusNode.IsRunning())
}
@ -90,7 +90,7 @@ func (s *BackendTestSuite) StartTestBackend(opts ...TestNodeOption) {
for i := range opts {
opts[i](nodeConfig)
}
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
// import account keys
s.NoError(importTestAccounts(nodeConfig.KeyStoreDir))

View File

@ -30,7 +30,7 @@ func (s *WhisperExtensionSuite) SetupTest() {
cfg, err := utils.MakeTestNodeConfigWithDataDir(fmt.Sprintf("test-shhext-%d", i), dir, 777)
s.Require().NoError(err)
s.nodes[i] = node.New()
s.Require().NoError(s.nodes[i].Start(cfg))
s.Require().NoError(s.nodes[i].Start(cfg, nil))
}
}

View File

@ -708,7 +708,10 @@ func (s *WhisperMailboxSuite) startBackend(name string) (*api.StatusBackend, fun
backend := api.NewStatusBackend()
nodeConfig, err := utils.MakeTestNodeConfig(utils.GetNetworkID())
nodeConfig.DataDir = datadir
nodeConfig.KeyStoreDir = filepath.Join(datadir, "keystore")
s.Require().NoError(err)
s.Require().NoError(backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
s.Require().False(backend.IsNodeRunning())
nodeConfig.WhisperConfig.LightClient = true
@ -748,6 +751,7 @@ func (s *WhisperMailboxSuite) startMailboxBackendWithCallback(
s.Require().NoError(err)
mailboxBackend := api.NewStatusBackend()
s.Require().NoError(mailboxBackend.AccountManager().InitKeystore(mailboxConfig.KeyStoreDir))
datadir := filepath.Join(utils.RootDir, ".ethereumtest/mailbox", name)
mailboxConfig.LightEthConfig.Enabled = false

View File

@ -40,7 +40,7 @@ func (s *WhisperTestSuite) TestWhisperFilterRace() {
whisperService, err := s.Backend.StatusNode().WhisperService()
s.NoError(err)
accountManager := account.NewManager(s.Backend.StatusNode())
accountManager := s.Backend.AccountManager()
s.NotNil(accountManager)
whisperAPI := whisper.NewPublicWhisperAPI(whisperService)