From be9c55bc16ca035ca9bfc0586b6c9298a91bdcd4 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Tue, 20 Aug 2019 18:38:40 +0300 Subject: [PATCH] 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 --- Makefile | 1 - account/accounts.go | 86 +-- account/accounts_mock.go | 65 --- account/accounts_test.go | 300 ++++------ account/keystore.go | 25 + account/utils.go | 2 - api/backend.go | 194 ++++++- api/backend_subs_test.go | 28 +- api/backend_test.go | 16 +- api/utils_test.go | 1 + lib/library.go | 102 +++- lib/library_test.go | 5 +- lib/library_test_multiaccount.go | 39 +- lib/library_test_utils.go | 515 +++++------------- mobile/status.go | 104 +++- multiaccounts/accounts/database.go | 168 ++++++ multiaccounts/accounts/database_test.go | 158 ++++++ multiaccounts/accounts/doc.go | 6 + multiaccounts/accounts/migrations/bindata.go | 127 +++++ multiaccounts/accounts/migrations/migrate.go | 18 + .../migrations/sql/0001_settings.down.sql | 2 + .../migrations/sql/0001_settings.up.sql | 19 + multiaccounts/accounts/migrations/sql/doc.go | 3 + multiaccounts/database.go | 77 +++ multiaccounts/database_test.go | 62 +++ multiaccounts/migrations/bindata.go | 127 +++++ multiaccounts/migrations/migrate.go | 18 + .../migrations/sql/0001_accounts.down.sql | 1 + .../migrations/sql/0001_accounts.up.sql | 6 + multiaccounts/migrations/sql/doc.go | 3 + node/node.go | 24 +- node/node_api_test.go | 5 +- node/node_test.go | 7 +- node/status_node.go | 48 +- node/status_node_rpc_client_test.go | 2 +- node/status_node_test.go | 32 +- services/accounts/README.md | 38 ++ services/accounts/accounts.go | 24 + services/accounts/multiaccounts.go | 34 ++ services/accounts/service.go | 57 ++ services/accounts/settings.go | 34 ++ signal/events_node.go | 7 + sqlite/fields.go | 39 ++ sqlite/sqlite.go | 44 +- t/devtests/devnode.go | 1 + t/e2e/accounts/accounts_test.go | 10 +- t/e2e/api/api_test.go | 5 +- t/e2e/api/backend_test.go | 3 +- t/e2e/node/manager_test.go | 34 +- t/e2e/rpc/rpc_test.go | 6 +- t/e2e/services/base_api_test.go | 1 + t/e2e/suites.go | 4 +- t/e2e/whisper/whisper_ext_test.go | 2 +- t/e2e/whisper/whisper_mailbox_test.go | 4 + t/e2e/whisper/whisper_test.go | 2 +- 55 files changed, 1838 insertions(+), 907 deletions(-) delete mode 100644 account/accounts_mock.go create mode 100644 account/keystore.go create mode 100644 multiaccounts/accounts/database.go create mode 100644 multiaccounts/accounts/database_test.go create mode 100644 multiaccounts/accounts/doc.go create mode 100644 multiaccounts/accounts/migrations/bindata.go create mode 100644 multiaccounts/accounts/migrations/migrate.go create mode 100644 multiaccounts/accounts/migrations/sql/0001_settings.down.sql create mode 100644 multiaccounts/accounts/migrations/sql/0001_settings.up.sql create mode 100644 multiaccounts/accounts/migrations/sql/doc.go create mode 100644 multiaccounts/database.go create mode 100644 multiaccounts/database_test.go create mode 100644 multiaccounts/migrations/bindata.go create mode 100644 multiaccounts/migrations/migrate.go create mode 100644 multiaccounts/migrations/sql/0001_accounts.down.sql create mode 100644 multiaccounts/migrations/sql/0001_accounts.up.sql create mode 100644 multiaccounts/migrations/sql/doc.go create mode 100644 services/accounts/README.md create mode 100644 services/accounts/accounts.go create mode 100644 services/accounts/multiaccounts.go create mode 100644 services/accounts/service.go create mode 100644 services/accounts/settings.go create mode 100644 sqlite/fields.go diff --git a/Makefile b/Makefile index af917ea43..c0e895da9 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/account/accounts.go b/account/accounts.go index 64f9c7177..73af4753a 100644 --- a/account/accounts.go +++ b/account/accounts.go @@ -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) } diff --git a/account/accounts_mock.go b/account/accounts_mock.go deleted file mode 100644 index 7d04f53db..000000000 --- a/account/accounts_mock.go +++ /dev/null @@ -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)) -} diff --git a/account/accounts_test.go b/account/accounts_test.go index d1133f0be..b5db22677 100644 --- a/account/accounts_test.go +++ b/account/accounts_test.go @@ -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) } } diff --git a/account/keystore.go b/account/keystore.go new file mode 100644 index 000000000..40e4f8971 --- /dev/null +++ b/account/keystore.go @@ -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 +} diff --git a/account/utils.go b/account/utils.go index bddd92da5..703ed64c6 100644 --- a/account/utils.go +++ b/account/utils.go @@ -42,7 +42,6 @@ func ParseLoginParams(paramsJSON string) (LoginParams, error) { params LoginParams zeroAddress common.Address ) - if err := json.Unmarshal([]byte(paramsJSON), ¶ms); err != nil { return params, err } @@ -60,7 +59,6 @@ func ParseLoginParams(paramsJSON string) (LoginParams, error) { return params, newErrZeroAddress("WatchAddresses") } } - return params, nil } diff --git a/api/backend.go b/api/backend.go index cb34d2473..a98b2ed52 100644 --- a/api/backend.go +++ b/api/backend.go @@ -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(), diff --git a/api/backend_subs_test.go b/api/backend_subs_test.go index af013573b..a776e5dd5 100644 --- a/api/backend_subs_test.go +++ b/api/backend_subs_test.go @@ -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) diff --git a/api/backend_test.go b/api/backend_test.go index 7cdcc8eff..6f6a3b196 100644 --- a/api/backend_test.go +++ b/api/backend_test.go @@ -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() { diff --git a/api/utils_test.go b/api/utils_test.go index c90ae7e03..f95ff6b61 100644 --- a/api/utils_test.go +++ b/api/utils_test.go @@ -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() { diff --git a/lib/library.go b/lib/library.go index 90281685a..00c66f6c8 100644 --- a/lib/library.go +++ b/lib/library.go @@ -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 diff --git a/lib/library_test.go b/lib/library_test.go index 45afa10b2..7b1c31160 100644 --- a/lib/library_test.go +++ b/lib/library_test.go @@ -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) { diff --git a/lib/library_test_multiaccount.go b/lib/library_test_multiaccount.go index c216aa5f4..9b5b693f1 100644 --- a/lib/library_test_multiaccount.go +++ b/lib/library_test_multiaccount.go @@ -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") diff --git a/lib/library_test_utils.go b/lib/library_test_utils.go index 2fa2355a4..0dd772203 100644 --- a/lib/library_test_utils.go +++ b/lib/library_test_utils.go @@ -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)) diff --git a/mobile/status.go b/mobile/status.go index 18382b3f4..1644272d4 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -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 diff --git a/multiaccounts/accounts/database.go b/multiaccounts/accounts/database.go new file mode 100644 index 000000000..c683a394f --- /dev/null +++ b/multiaccounts/accounts/database.go @@ -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 +} diff --git a/multiaccounts/accounts/database_test.go b/multiaccounts/accounts/database_test.go new file mode 100644 index 000000000..3eb50dca6 --- /dev/null +++ b/multiaccounts/accounts/database_test.go @@ -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) +} diff --git a/multiaccounts/accounts/doc.go b/multiaccounts/accounts/doc.go new file mode 100644 index 000000000..29ac65e9b --- /dev/null +++ b/multiaccounts/accounts/doc.go @@ -0,0 +1,6 @@ +package accounts + +const ( + // NodeConfigTag tag for a node configuration. + NodeConfigTag = "node-config" +) diff --git a/multiaccounts/accounts/migrations/bindata.go b/multiaccounts/accounts/migrations/bindata.go new file mode 100644 index 000000000..d3d79eb89 --- /dev/null +++ b/multiaccounts/accounts/migrations/bindata.go @@ -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{ + }}, +}} diff --git a/multiaccounts/accounts/migrations/migrate.go b/multiaccounts/accounts/migrations/migrate.go new file mode 100644 index 000000000..825e1ddfc --- /dev/null +++ b/multiaccounts/accounts/migrations/migrate.go @@ -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) + }, + )) +} diff --git a/multiaccounts/accounts/migrations/sql/0001_settings.down.sql b/multiaccounts/accounts/migrations/sql/0001_settings.down.sql new file mode 100644 index 000000000..f0df88c85 --- /dev/null +++ b/multiaccounts/accounts/migrations/sql/0001_settings.down.sql @@ -0,0 +1,2 @@ +DROP TABLE settings; +DROP TABLE accounts; diff --git a/multiaccounts/accounts/migrations/sql/0001_settings.up.sql b/multiaccounts/accounts/migrations/sql/0001_settings.up.sql new file mode 100644 index 000000000..66314ae37 --- /dev/null +++ b/multiaccounts/accounts/migrations/sql/0001_settings.up.sql @@ -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); diff --git a/multiaccounts/accounts/migrations/sql/doc.go b/multiaccounts/accounts/migrations/sql/doc.go new file mode 100644 index 000000000..e0a060394 --- /dev/null +++ b/multiaccounts/accounts/migrations/sql/doc.go @@ -0,0 +1,3 @@ +package sql + +//go:generate go-bindata -pkg migrations -o ../bindata.go ./ diff --git a/multiaccounts/database.go b/multiaccounts/database.go new file mode 100644 index 000000000..758149a96 --- /dev/null +++ b/multiaccounts/database.go @@ -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 +} diff --git a/multiaccounts/database_test.go b/multiaccounts/database_test.go new file mode 100644 index 000000000..724ab85c7 --- /dev/null +++ b/multiaccounts/database_test.go @@ -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) +} diff --git a/multiaccounts/migrations/bindata.go b/multiaccounts/migrations/bindata.go new file mode 100644 index 000000000..cc45a6696 --- /dev/null +++ b/multiaccounts/migrations/bindata.go @@ -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{ + }}, +}} diff --git a/multiaccounts/migrations/migrate.go b/multiaccounts/migrations/migrate.go new file mode 100644 index 000000000..825e1ddfc --- /dev/null +++ b/multiaccounts/migrations/migrate.go @@ -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) + }, + )) +} diff --git a/multiaccounts/migrations/sql/0001_accounts.down.sql b/multiaccounts/migrations/sql/0001_accounts.down.sql new file mode 100644 index 000000000..5eb8e054f --- /dev/null +++ b/multiaccounts/migrations/sql/0001_accounts.down.sql @@ -0,0 +1 @@ +DROP TABLE accounts; diff --git a/multiaccounts/migrations/sql/0001_accounts.up.sql b/multiaccounts/migrations/sql/0001_accounts.up.sql new file mode 100644 index 000000000..8f18ee4c2 --- /dev/null +++ b/multiaccounts/migrations/sql/0001_accounts.up.sql @@ -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; diff --git a/multiaccounts/migrations/sql/doc.go b/multiaccounts/migrations/sql/doc.go new file mode 100644 index 000000000..e0a060394 --- /dev/null +++ b/multiaccounts/migrations/sql/doc.go @@ -0,0 +1,3 @@ +package sql + +//go:generate go-bindata -pkg migrations -o ../bindata.go ./ diff --git a/node/node.go b/node/node.go index 882a65429..140a10e91 100644 --- a/node/node.go +++ b/node/node.go @@ -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, ðConf) + // 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, ðConf) }) } -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 }) } diff --git a/node/node_api_test.go b/node/node_api_test.go index f5a2c06fa..7ab62ebbc 100644 --- a/node/node_api_test.go +++ b/node/node_api_test.go @@ -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()) }() diff --git a/node/node_test.go b/node/node_test.go index 553d4ab58..c8fcb6640 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -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) } diff --git a/node/status_node.go b/node/status_node.go index 23bc5358a..280acc286 100644 --- a/node/status_node.go +++ b/node/status_node.go @@ -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() diff --git a/node/status_node_rpc_client_test.go b/node/status_node_rpc_client_test.go index a7aa81bdf..b29c2803d 100644 --- a/node/status_node_rpc_client_test.go +++ b/node/status_node_rpc_client_test.go @@ -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) { diff --git a/node/status_node_test.go b/node/status_node_test.go index 283399bf5..9fd879cda 100644 --- a/node/status_node_test.go +++ b/node/status_node_test.go @@ -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()) diff --git a/services/accounts/README.md b/services/accounts/README.md new file mode 100644 index 000000000..0858c0bab --- /dev/null +++ b/services/accounts/README.md @@ -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 \ No newline at end of file diff --git a/services/accounts/accounts.go b/services/accounts/accounts.go new file mode 100644 index 000000000..34c352aa6 --- /dev/null +++ b/services/accounts/accounts.go @@ -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() +} diff --git a/services/accounts/multiaccounts.go b/services/accounts/multiaccounts.go new file mode 100644 index 000000000..eda42f92a --- /dev/null +++ b/services/accounts/multiaccounts.go @@ -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) +} diff --git a/services/accounts/service.go b/services/accounts/service.go new file mode 100644 index 000000000..9a7f08821 --- /dev/null +++ b/services/accounts/service.go @@ -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 +} diff --git a/services/accounts/settings.go b/services/accounts/settings.go new file mode 100644 index 000000000..a06894534 --- /dev/null +++ b/services/accounts/settings.go @@ -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) +} diff --git a/signal/events_node.go b/signal/events_node.go index 00147d1b7..6766b4b5c 100644 --- a/signal/events_node.go +++ b/signal/events_node.go @@ -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) +} diff --git a/sqlite/fields.go b/sqlite/fields.go new file mode 100644 index 000000000..b0fe54faa --- /dev/null +++ b/sqlite/fields.go @@ -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) +} diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index c1b5cd007..3a1fb1bdf 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -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 +} diff --git a/t/devtests/devnode.go b/t/devtests/devnode.go index 89b6a97e9..674ccc309 100644 --- a/t/devtests/devnode.go +++ b/t/devtests/devnode.go @@ -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) diff --git a/t/e2e/accounts/accounts_test.go b/t/e2e/accounts/accounts_test.go index 9f379f8fe..2b1746517 100644 --- a/t/e2e/accounts/accounts_test.go +++ b/t/e2e/accounts/accounts_test.go @@ -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) diff --git a/t/e2e/api/api_test.go b/t/e2e/api/api_test.go index 9186ac7d8..fd2e7a8d7 100644 --- a/t/e2e/api/api_test.go +++ b/t/e2e/api/api_test.go @@ -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 { diff --git a/t/e2e/api/backend_test.go b/t/e2e/api/backend_test.go index c91a1bf07..4c65c93a7 100644 --- a/t/e2e/api/backend_test.go +++ b/t/e2e/api/backend_test.go @@ -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)) diff --git a/t/e2e/node/manager_test.go b/t/e2e/node/manager_test.go index 4b10efd14..1c47c2946 100644 --- a/t/e2e/node/manager_test.go +++ b/t/e2e/node/manager_test.go @@ -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 diff --git a/t/e2e/rpc/rpc_test.go b/t/e2e/rpc/rpc_test.go index b194312c0..b6e13504c 100644 --- a/t/e2e/rpc/rpc_test.go +++ b/t/e2e/rpc/rpc_test.go @@ -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) diff --git a/t/e2e/services/base_api_test.go b/t/e2e/services/base_api_test.go index bbe115734..a77f1cbc1 100644 --- a/t/e2e/services/base_api_test.go +++ b/t/e2e/services/base_api_test.go @@ -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 diff --git a/t/e2e/suites.go b/t/e2e/suites.go index 161f88e59..aa9613a29 100644 --- a/t/e2e/suites.go +++ b/t/e2e/suites.go @@ -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)) diff --git a/t/e2e/whisper/whisper_ext_test.go b/t/e2e/whisper/whisper_ext_test.go index e19c6a03b..a3a889142 100644 --- a/t/e2e/whisper/whisper_ext_test.go +++ b/t/e2e/whisper/whisper_ext_test.go @@ -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)) } } diff --git a/t/e2e/whisper/whisper_mailbox_test.go b/t/e2e/whisper/whisper_mailbox_test.go index d51db01c6..24dd3ec1b 100644 --- a/t/e2e/whisper/whisper_mailbox_test.go +++ b/t/e2e/whisper/whisper_mailbox_test.go @@ -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 diff --git a/t/e2e/whisper/whisper_test.go b/t/e2e/whisper/whisper_test.go index 1521a0b77..1e8bf98d5 100644 --- a/t/e2e/whisper/whisper_test.go +++ b/t/e2e/whisper/whisper_test.go @@ -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)