Accounts data management (#1530)

* WIP accounts implementation

* Accounts datasore and changes to status mobile API

* Add library changes and method to update config

* Handle error after account selection

* Add two methods to start account to backend

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

* Resolve linter warning

* Bring back StartNode StopNode for tests

* Add sub accounts and get/save api

* Changes to accounts structure

* Login use root address and fetch necessary info from database

* Cover accounts store with tests

* Refactor in progress

* Initialize status keystore instance before starting ethereum node

* Rework library tests

* Resolve failures in private api test and send transaction test

* Pass pointer to initialized config to unmarshal

* Use multiaccounts/accounts naming consistently

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

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

* Add photo-path field for multiaccount struct

* Add multiaccoutns rpc with updateAccount method

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

* Fix linter in services/accounts

* Select account before starting a node

* Save list of accounts on first login

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

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

View File

@ -236,7 +236,6 @@ mock-install: ##@other Install mocking tools
mock: ##@other Regenerate mocks mock: ##@other Regenerate mocks
mockgen -package=fcm -destination=notifications/push/fcm/client_mock.go -source=notifications/push/fcm/client.go 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=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=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 mockgen -package=peer -destination=services/peer/discoverer_mock.go -source=services/peer/service.go

View File

@ -30,21 +30,16 @@ var (
ErrInvalidMasterKeyCreated = errors.New("can not create master extended key") ErrInvalidMasterKeyCreated = errors.New("can not create master extended key")
ErrOnboardingNotStarted = errors.New("onboarding must be started before choosing an account") ErrOnboardingNotStarted = errors.New("onboarding must be started before choosing an account")
ErrOnboardingAccountNotFound = errors.New("cannot find onboarding account with the given id") ErrOnboardingAccountNotFound = errors.New("cannot find onboarding account with the given id")
ErrAccountKeyStoreMissing = errors.New("account key store is not set")
) )
var zeroAddress = common.Address{} 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. // Manager represents account manager interface.
type Manager struct { type Manager struct {
geth GethServiceProvider mu sync.RWMutex
keystore *keystore.KeyStore
mu sync.RWMutex manager *accounts.Manager
accountsGenerator *generator.Generator accountsGenerator *generator.Generator
onboarding *Onboarding onboarding *Onboarding
@ -55,15 +50,43 @@ type Manager struct {
} }
// NewManager returns new node account manager. // NewManager returns new node account manager.
func NewManager(geth GethServiceProvider) *Manager { func NewManager() *Manager {
manager := &Manager{ m := &Manager{}
geth: geth, 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) func (m *Manager) GetKeystore() *keystore.KeyStore {
manager.accountsGenerator = accountsGenerator 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. // AccountsGenerator returns accountsGenerator.
@ -270,24 +293,22 @@ func (m *Manager) Logout() {
// ImportAccount imports the account specified with privateKey. // ImportAccount imports the account specified with privateKey.
func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error) { func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error) {
keyStore, err := m.geth.AccountKeyStore() if m.keystore == nil {
if err != nil { return common.Address{}, ErrAccountKeyStoreMissing
return common.Address{}, err
} }
account, err := keyStore.ImportECDSA(privateKey, password) account, err := m.keystore.ImportECDSA(privateKey, password)
return account.Address, err return account.Address, err
} }
func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) { func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
keyStore, err := m.geth.AccountKeyStore() if m.keystore == nil {
if err != nil { return "", "", ErrAccountKeyStoreMissing
return "", "", err
} }
// imports extended key, create key file (if necessary) // imports extended key, create key file (if necessary)
account, err := keyStore.ImportSingleExtendedKey(extKey, password) account, err := m.keystore.ImportSingleExtendedKey(extKey, password)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -295,7 +316,7 @@ func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password
address = account.Address.Hex() address = account.Address.Hex()
// obtain public key to return // obtain public key to return
account, key, err := keyStore.AccountDecryptedKey(account, password) account, key, err := m.keystore.AccountDecryptedKey(account, password)
if err != nil { if err != nil {
return address, "", err 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. // 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. // 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) { func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
keyStore, err := m.geth.AccountKeyStore() if m.keystore == nil {
if err != nil { return "", "", ErrAccountKeyStoreMissing
return "", "", err
} }
// imports extended key, create key file (if necessary) // 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 { if err != nil {
return "", "", err return "", "", err
} }
address = account.Address.Hex() address = account.Address.Hex()
// obtain public key to return // obtain public key to return
account, key, err := keyStore.AccountDecryptedKey(account, password) account, key, err := m.keystore.AccountDecryptedKey(account, password)
if err != nil { if err != nil {
return address, "", err return address, "", err
} }
@ -335,7 +355,6 @@ func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extke
func (m *Manager) Accounts() ([]gethcommon.Address, error) { func (m *Manager) Accounts() ([]gethcommon.Address, error) {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
addresses := make([]gethcommon.Address, 0) addresses := make([]gethcommon.Address, 0)
if m.mainAccountAddress != zeroAddress { if m.mainAccountAddress != zeroAddress {
addresses = append(addresses, m.mainAccountAddress) 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 // 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. // 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) { func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) {
keyStore, err := m.geth.AccountKeyStore() if m.keystore == nil {
if err != nil { return accounts.Account{}, nil, ErrAccountKeyStoreMissing
return accounts.Account{}, nil, err
} }
account, err := ParseAccountString(address) account, err := ParseAccountString(address)
@ -408,7 +426,7 @@ func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.
} }
var key *keystore.Key var key *keystore.Key
account, key, err = keyStore.AccountDecryptedKey(account, password) account, key, err = m.keystore.AccountDecryptedKey(account, password)
if err != nil { if err != nil {
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err) err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)
} }

View File

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

View File

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

25
account/keystore.go Normal file
View File

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

View File

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

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"path" "path"
"path/filepath"
"sync" "sync"
"time" "time"
@ -19,11 +20,15 @@ import (
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/account" "github.com/status-im/status-go/account"
"github.com/status-im/status-go/crypto" "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/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/node"
"github.com/status-im/status-go/notifications/push/fcm" "github.com/status-im/status-go/notifications/push/fcm"
"github.com/status-im/status-go/params" "github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc" "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/personal"
"github.com/status-im/status-go/services/rpcfilters" "github.com/status-im/status-go/services/rpcfilters"
"github.com/status-im/status-go/services/subscriptions" "github.com/status-im/status-go/services/subscriptions"
@ -52,10 +57,14 @@ var (
// StatusBackend implements Status.im service // StatusBackend implements Status.im service
type StatusBackend struct { type StatusBackend struct {
mu sync.Mutex mu sync.Mutex
// rootDataDir is the same for all networks.
rootDataDir string
accountsDB *accounts.Database
statusNode *node.StatusNode statusNode *node.StatusNode
personalAPI *personal.PublicAPI personalAPI *personal.PublicAPI
rpcFilters *rpcfilters.Service rpcFilters *rpcfilters.Service
multiaccountsDB *multiaccounts.Database
accountManager *account.Manager accountManager *account.Manager
transactor *transactions.Transactor transactor *transactions.Transactor
newNotification fcm.NotificationConstructor newNotification fcm.NotificationConstructor
@ -70,12 +79,11 @@ func NewStatusBackend() *StatusBackend {
defer log.Info("Status backend initialized", "version", params.Version, "commit", params.GitCommit) defer log.Info("Status backend initialized", "version", params.Version, "commit", params.GitCommit)
statusNode := node.New() statusNode := node.New()
accountManager := account.NewManager(statusNode) accountManager := account.NewManager()
transactor := transactions.NewTransactor() transactor := transactions.NewTransactor()
personalAPI := personal.NewAPI() personalAPI := personal.NewAPI()
notificationManager := fcm.NewNotification(fcmServerKey) notificationManager := fcm.NewNotification(fcmServerKey)
rpcFilters := rpcfilters.New(statusNode) rpcFilters := rpcfilters.New(statusNode)
return &StatusBackend{ return &StatusBackend{
statusNode: statusNode, statusNode: statusNode,
accountManager: accountManager, accountManager: accountManager,
@ -121,6 +129,144 @@ func (b *StatusBackend) StartNode(config *params.NodeConfig) error {
return nil 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 { func (b *StatusBackend) rpcFiltersService() gethnode.ServiceConstructor {
return func(*gethnode.ServiceContext) (gethnode.Service, error) { return func(*gethnode.ServiceContext) (gethnode.Service, error) {
return rpcfilters.New(b.statusNode), nil 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) { func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -144,17 +296,22 @@ func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return err return err
} }
services := []gethnode.ServiceConstructor{} services := []gethnode.ServiceConstructor{}
services = appendIf(config.UpstreamConfig.Enabled, services, b.rpcFiltersService()) services = appendIf(config.UpstreamConfig.Enabled, services, b.rpcFiltersService())
services = append(services, b.subscriptionService()) 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{ if err = b.statusNode.StartWithOptions(config, node.StartOptions{
Services: services, Services: services,
// The peers discovery protocols are started manually after // The peers discovery protocols are started manually after
// `node.ready` signal is sent. // `node.ready` signal is sent.
// It was discussed in https://github.com/status-im/status-go/pull/1333. // It was discussed in https://github.com/status-im/status-go/pull/1333.
StartDiscovery: false, StartDiscovery: false,
AccountsManager: manager,
}); err != nil { }); err != nil {
return return
} }
@ -484,6 +641,22 @@ func (b *StatusBackend) Logout() error {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() 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() whisperService, err := b.statusNode.WhisperService()
switch err { switch err {
case node.ErrServiceUnknown: // Whisper was never registered case node.ErrServiceUnknown: // Whisper was never registered
@ -521,9 +694,15 @@ func (b *StatusBackend) Logout() error {
return err 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 return nil
} }
@ -622,7 +801,6 @@ func (b *StatusBackend) startWallet(password string) error {
allAddresses := make([]common.Address, len(watchAddresses)+1) allAddresses := make([]common.Address, len(watchAddresses)+1)
allAddresses[0] = mainAccountAddress allAddresses[0] = mainAccountAddress
copy(allAddresses[1:], watchAddresses) copy(allAddresses[1:], watchAddresses)
path := path.Join(b.statusNode.Config().DataDir, fmt.Sprintf("wallet-%x.sql", mainAccountAddress)) path := path.Join(b.statusNode.Config().DataDir, fmt.Sprintf("wallet-%x.sql", mainAccountAddress))
return wallet.StartReactor(path, password, return wallet.StartReactor(path, password,
b.statusNode.RPCClient().Ethclient(), b.statusNode.RPCClient().Ethclient(),

View File

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

View File

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

View File

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

View File

@ -12,10 +12,11 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "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/api"
"github.com/status-im/status-go/exportlogs" "github.com/status-im/status-go/exportlogs"
"github.com/status-im/status-go/logutils" "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/params"
"github.com/status-im/status-go/profiling" "github.com/status-im/status-go/profiling"
"github.com/status-im/status-go/services/personal" "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. // All general log messages in this package should be routed through this logger.
var logger = log.New("package", "status-go/lib") var logger = log.New("package", "status-go/lib")
//StartNode - start Status node // OpenAccounts opens database and returns accounts list.
//export StartNode //export OpenAccounts
func StartNode(configJSON *C.char) *C.char { func OpenAccounts(datadir *C.char) *C.char {
config, err := params.NewConfigFromJSON(C.GoString(configJSON)) statusBackend.UpdateRootDataDir(C.GoString(datadir))
err := statusBackend.OpenAccounts()
if err != nil { if err != nil {
return makeJSONResponse(err) return makeJSONResponse(err)
} }
accs, err := statusBackend.GetAccounts()
if err := logutils.OverrideRootLogWithConfig(config, false); err != nil { if err != nil {
return makeJSONResponse(err) return makeJSONResponse(err)
} }
data, err := json.Marshal(accs)
api.RunAsync(func() error { return statusBackend.StartNode(config) }) if err != nil {
return makeJSONResponse(err)
return makeJSONResponse(nil) }
} return C.CString(string(data))
//StopNode - stop status node
//export StopNode
func StopNode() *C.char {
api.RunAsync(statusBackend.StopNode)
return makeJSONResponse(nil)
} }
// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature // 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) 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 //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 // if verified, purges all the previous identities from Whisper, and injects verified key as shh identity
//export Login //export Login
func Login(loginParamsJSON *C.char) *C.char { func Login(accountData, password *C.char) *C.char {
params, err := account.ParseLoginParams(C.GoString(loginParamsJSON)) data, pass := C.GoString(accountData), C.GoString(password)
var account multiaccounts.Account
err := json.Unmarshal([]byte(data), &account)
if err != nil { 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) return makeJSONResponse(err)
} }
@ -349,7 +401,11 @@ func LoginWithKeycard(chatKeyData, encryptionKeyData *C.char) *C.char {
//export Logout //export Logout
func Logout() *C.char { func Logout() *C.char {
err := statusBackend.Logout() 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 // SignMessage unmarshals rpc params {data, address, password} and passes

View File

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

View File

@ -14,6 +14,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/account/generator" "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 func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T, feed *event.Feed) 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)
}
params := C.CString(`{ params := C.CString(`{
"n": 2, "n": 2,
"mnemonicPhraseLength": 24, "mnemonicPhraseLength": 24,
@ -90,7 +86,6 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
return false return false
} }
} }
password := "multi-account-test-password" password := "multi-account-test-password"
// store 2 derived child accounts from the first account. // store 2 derived child accounts from the first account.
@ -115,9 +110,7 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
return false return false
} }
} }
rawResp = MultiAccountReset() rawResp = MultiAccountReset()
// try again deriving addresses. // try again deriving addresses.
// it should fail because reset should remove all the accounts from memory. // it should fail because reset should remove all the accounts from memory.
for _, loadedID := range loadedIDs { for _, loadedID := range loadedIDs {
@ -126,16 +119,10 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
return false return false
} }
} }
return true return true
} }
func testMultiAccountImportMnemonicAndDerive(t *testing.T) bool { //nolint: gocyclo func testMultiAccountImportMnemonicAndDerive(t *testing.T, feed *event.Feed) 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)
}
mnemonicPhrase := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" mnemonicPhrase := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
bip39Passphrase := "TREZOR" bip39Passphrase := "TREZOR"
params := mobile.MultiAccountImportMnemonicParams{ params := mobile.MultiAccountImportMnemonicParams{
@ -210,7 +197,6 @@ func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []str
addresses[path] = info.Address addresses[path] = info.Address
} }
return addresses, true 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. // 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 { for _, address := range addresses {
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password) _, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password)
if err != nil { if err != nil {
@ -259,12 +246,7 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, password strin
return addresses, true return addresses, true
} }
func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo func testMultiAccountGenerateAndDerive(t *testing.T, feed *event.Feed) 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)
}
paths := []string{"m/0", "m/1"} paths := []string{"m/0", "m/1"}
params := mobile.MultiAccountGenerateAndDeriveAddressesParams{ params := mobile.MultiAccountGenerateAndDeriveAddressesParams{
MultiAccountGenerateParams: mobile.MultiAccountGenerateParams{ MultiAccountGenerateParams: mobile.MultiAccountGenerateParams{
@ -303,12 +285,7 @@ func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
return true return true
} }
func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo func testMultiAccountImportStore(t *testing.T, feed *event.Feed) 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)
}
key, err := crypto.GenerateKey() key, err := crypto.GenerateKey()
if err != nil { if err != nil {
t.Errorf("failed generating key") t.Errorf("failed generating key")
@ -350,7 +327,7 @@ func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
// check the response doesn't have errors // check the response doesn't have errors
checkMultiAccountResponse(t, rawResp, &storeResp) checkMultiAccountResponse(t, rawResp, &storeResp)
dir := statusBackend.StatusNode().Config().DataDir dir := keystoreDir
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, storeResp.Address, password) _, err = statusBackend.AccountManager().VerifyAccountPassword(dir, storeResp.Address, password)
if err != nil { if err != nil {
t.Errorf("failed to verify password on stored derived account") t.Errorf("failed to verify password on stored derived account")

View File

@ -9,8 +9,8 @@
package main package main
import "C"
import ( import (
"C"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -31,10 +31,14 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/account" "github.com/status-im/status-go/account"
"github.com/status-im/status-go/signal" "github.com/status-im/status-go/signal"
. "github.com/status-im/status-go/t/utils" //nolint: golint . "github.com/status-im/status-go/t/utils" //nolint: golint
"github.com/status-im/status-go/transactions" "github.com/status-im/status-go/transactions"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/event"
) )
import "github.com/status-im/status-go/multiaccounts/accounts"
const initJS = ` const initJS = `
var _status_catalog = { var _status_catalog = {
@ -44,15 +48,27 @@ const initJS = `
var ( var (
zeroHash = gethcommon.Hash{} zeroHash = gethcommon.Hash{}
testChainDir string testChainDir string
keystoreDir string
nodeConfigJSON string nodeConfigJSON string
) )
func buildLoginParamsJSON(chatAddress, password string) *C.char { func buildAccountData(name, chatAddress string) *C.char {
return C.CString(fmt.Sprintf(`{ return C.CString(fmt.Sprintf(`{
"chatAddress": "%s", "name": "%s",
"password": "%s", "address": "%s"
"mainAccount": "%s" }`, name, chatAddress))
}`, chatAddress, password, 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 { 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() { func init() {
testChainDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()]) testChainDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
keystoreDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()], "keystore")
nodeConfigJSON = `{ nodeConfigJSON = `{
"NetworkId": ` + strconv.Itoa(GetNetworkID()) + `, "NetworkId": ` + strconv.Itoa(GetNetworkID()) + `,
@ -73,6 +107,7 @@ func init() {
"HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `, "HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `,
"LogLevel": "INFO", "LogLevel": "INFO",
"NoDiscovery": true, "NoDiscovery": true,
"APIModules": "web3,eth",
"LightEthConfig": { "LightEthConfig": {
"Enabled": true "Enabled": true
}, },
@ -87,106 +122,123 @@ func init() {
}` }`
} }
// nolint: deadcode func createAccountAndLogin(t *testing.T, feed *event.Feed) account.Info {
func testExportedAPI(t *testing.T, done chan struct{}) { account1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
<-startTestNode(t) require.NoError(t, err)
defer func() { t.Logf("account created: {address: %s, key: %s}", account1.WalletAddress, account1.WalletPubKey)
done <- struct{}{}
}()
// prepare accounts // select account
testKeyDir := filepath.Join(testChainDir, "keystore") loginResponse := APIResponse{}
if err := ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil { rawResponse := SaveAccountAndLogin(buildAccountData("test", account1.WalletAddress), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(account1.WalletAddress))
panic(err) require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
} require.Empty(t, loginResponse.Error)
if err := ImportTestAccount(testKeyDir, GetAccount2PKFile()); err != nil { require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
panic(err) 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. // 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 // Probably, there should be a cleaner way, for example, test cgo bindings in e2e tests
// separately from other internal 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 { tests := []struct {
name string name string
fn func(t *testing.T) bool fn func(t *testing.T, feed *event.Feed) bool
}{ }{
{ {
"stop/resume node", "StopResumeNode",
testStopResumeNode, testStopResumeNode,
}, },
{ {
"call RPC on in-proc handler", "RPCInProc",
testCallRPC, testCallRPC,
}, },
{ {
"call private API using RPC", "RPCPrivateAPI",
testCallRPCWithPrivateAPI, testCallRPCWithPrivateAPI,
}, },
{ {
"call private API using private RPC client", "RPCPrivateClient",
testCallPrivateRPCWithPrivateAPI, testCallPrivateRPCWithPrivateAPI,
}, },
{ {
"verify account password", "VerifyAccountPassword",
testVerifyAccountPassword, testVerifyAccountPassword,
}, },
{ {
"recover account", "RecoverAccount",
testRecoverAccount, testRecoverAccount,
}, },
{ {
"account select/login", "LoginKeycard",
testAccountSelect,
},
{
"login with keycard",
testLoginWithKeycard, testLoginWithKeycard,
}, },
{ {
"account logout", "AccountLoout",
testAccountLogout, testAccountLogout,
}, },
{ {
"send transaction", "SendTransaction",
testSendTransaction, testSendTransactionWithLogin,
}, },
{ {
"send transaction with invalid password", "SendTransactionInvalidPassword",
testSendTransactionInvalidPassword, testSendTransactionInvalidPassword,
}, },
{ {
"failed single transaction", "SendTransactionFailed",
testFailedTransaction, testFailedTransaction,
}, },
{ {
"MultiAccount - Generate/Derive/StoreDerived/Load/Reset", "MultiAccount/Generate/Derive/StoreDerived/Load/Reset",
testMultiAccountGenerateDeriveStoreLoadReset, testMultiAccountGenerateDeriveStoreLoadReset,
}, },
{ {
"MultiAccount - ImportMnemonic/Derive", "MultiAccount/ImportMnemonic/Derive",
testMultiAccountImportMnemonicAndDerive, testMultiAccountImportMnemonicAndDerive,
}, },
{ {
"MultiAccount - GenerateAndDerive", "MultiAccount/GenerateAndDerive",
testMultiAccountGenerateAndDerive, testMultiAccountGenerateAndDerive,
}, },
{ {
"MultiAccount - Import/Store", "MultiAccount/Import/Store",
testMultiAccountImportStore, testMultiAccountImportStore,
}, },
} }
for _, test := range tests { for _, tc := range tests {
t.Logf("=== RUN %s", test.name) t.Run(tc.name, func(t *testing.T) {
if ok := test.fn(t); !ok { feed := &event.Feed{}
t.Logf("=== FAILED %s", test.name) signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
break 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") tmpDir, err := ioutil.TempDir(os.TempDir(), "accounts")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -225,137 +277,34 @@ func testVerifyAccountPassword(t *testing.T) bool {
return true return true
} }
//@TODO(adam): quarantined this test until it uses a different directory. func testStopResumeNode(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
//nolint: deadcode account1 := createAccountAndLogin(t, feed)
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)
}
whisperService, err := statusBackend.StatusNode().WhisperService() whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil { require.NoError(t, err)
t.Errorf("whisper service not running: %v", err) require.True(t, whisperService.HasKeyPair(account1.ChatPubKey), "whisper should have keypair")
}
// create an account response := APIResponse{}
account1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password) rawResponse := StopNode()
if err != nil { require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
t.Errorf("could not create account: %v", err) require.Empty(t, response.Error)
return false
}
t.Logf("account created: {address: %s, key: %s}", account1.WalletAddress, account1.WalletPubKey)
// make sure that identity is not (yet injected) require.NoError(t, waitSignal(feed, signal.EventNodeStopped, 3*time.Second))
if whisperService.HasKeyPair(account1.ChatPubKey) { response = APIResponse{}
t.Error("identity already present in whisper") rawResponse = StartNode(C.CString(nodeConfigJSON))
} require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
require.Empty(t, response.Error)
// select account require.NoError(t, waitSignal(feed, signal.EventNodeReady, 5*time.Second))
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)
// now, verify that we still have account logged in // now, verify that we still have account logged in
whisperService, err = statusBackend.StatusNode().WhisperService() whisperService, err = statusBackend.StatusNode().WhisperService()
if err != nil { require.NoError(t, err)
t.Errorf("whisper service not running: %v", err) require.True(t, whisperService.HasKeyPair(account1.ChatPubKey))
}
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)
return true 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"}` expected := `{"jsonrpc":"2.0","id":64,"result":"0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"}`
rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f20776f726c64"],"id":64}`)) rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f20776f726c64"],"id":64}`))
received := C.GoString(rawResponse) received := C.GoString(rawResponse)
@ -367,19 +316,16 @@ func testCallRPC(t *testing.T) bool {
return true 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"}}` 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}`)) rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
received := C.GoString(rawResponse) require.Equal(t, expected, C.GoString(rawResponse))
if expected != received {
t.Errorf("unexpected response: expected: %v, got: %v", expected, received)
return false
}
return true 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}`)) rawResponse := CallPrivateRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
received := C.GoString(rawResponse) received := C.GoString(rawResponse)
if strings.Contains(received, "error") { if strings.Contains(received, "error") {
@ -390,9 +336,9 @@ func testCallPrivateRPCWithPrivateAPI(t *testing.T) bool {
return true return true
} }
func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo func testRecoverAccount(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
keyStore, _ := statusBackend.StatusNode().AccountKeyStore() keyStore := statusBackend.AccountManager().GetKeystore()
require.NotNil(t, keyStore)
// create an account // create an account
accountInfo, mnemonic, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password) accountInfo, mnemonic, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
if err != nil { 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)") 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 // time to login with recovered data
whisperService, err := statusBackend.StatusNode().WhisperService() whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil { if err != nil {
t.Errorf("whisper service not running: %v", err) 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) { if !whisperService.HasKeyPair(chatPubKeyCheck) {
t.Errorf("identity not injected into whisper: %v", err) t.Errorf("identity not injected into whisper: %v", err)
} }
@ -525,91 +468,8 @@ func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
return true return true
} }
func testAccountSelect(t *testing.T) bool { //nolint: gocyclo func testLoginWithKeycard(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
// test to see if the account was injected in whisper createAccountAndLogin(t, feed)
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
chatPrivKey, err := crypto.GenerateKey() chatPrivKey, err := crypto.GenerateKey()
if err != nil { if err != nil {
t.Errorf("error generating chat key") t.Errorf("error generating chat key")
@ -656,32 +516,14 @@ func testLoginWithKeycard(t *testing.T) bool { //nolint: gocyclo
return true 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() whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil { if err != nil {
t.Errorf("whisper service not running: %v", err) t.Errorf("whisper service not running: %v", err)
return false 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) { if !whisperService.HasKeyPair(accountInfo.ChatPubKey) {
t.Error("identity not injected into whisper") t.Error("identity not injected into whisper")
return false return false
@ -714,15 +556,14 @@ type jsonrpcAnyResponse struct {
jsonrpcErrorResponse 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) 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{ args, err := json.Marshal(transactions.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.WalletAddress), From: account.FromAddress(TestConfig.Account1.WalletAddress),
To: account.ToAddress(TestConfig.Account2.WalletAddress), To: account.ToAddress(TestConfig.Account2.WalletAddress),
@ -752,7 +593,8 @@ func testSendTransaction(t *testing.T) bool {
return true return true
} }
func testSendTransactionInvalidPassword(t *testing.T) bool { func testSendTransactionInvalidPassword(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
EnsureNodeSync(statusBackend.StatusNode().EnsureSync) EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
// log into account from which transactions will be sent // log into account from which transactions will be sent
@ -789,7 +631,8 @@ func testSendTransactionInvalidPassword(t *testing.T) bool {
return true return true
} }
func testFailedTransaction(t *testing.T) bool { func testFailedTransaction(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
EnsureNodeSync(statusBackend.StatusNode().EnsureSync) EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
// log into wrong account in order to get selectedAccount error // 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 //nolint: deadcode
func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, APIDetailedResponse)) { func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, APIDetailedResponse)) {
result := ValidateNodeConfig(C.CString(config)) result := ValidateNodeConfig(C.CString(config))

View File

@ -10,10 +10,10 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "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/api"
"github.com/status-im/status-go/exportlogs" "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/params"
"github.com/status-im/status-go/profiling" "github.com/status-im/status-go/profiling"
"github.com/status-im/status-go/services/personal" "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. // All general log messages in this package should be routed through this logger.
var logger = log.New("package", "status-go/mobile") 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. // GenerateConfig for status node.
func GenerateConfig(datadir string, networkID int) string { func GenerateConfig(datadir string, networkID int) string {
config, err := params.NewNodeConfig(datadir, uint64(networkID)) config, err := params.NewNodeConfig(datadir, uint64(networkID))
@ -43,28 +61,6 @@ func GenerateConfig(datadir string, networkID int) string {
return string(outBytes) 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. // ExtractGroupMembershipSignatures extract public keys from tuples of content/signature.
func ExtractGroupMembershipSignatures(signaturePairsStr string) string { func ExtractGroupMembershipSignatures(signaturePairsStr string) string {
var signaturePairs [][2]string var signaturePairs [][2]string
@ -209,7 +205,6 @@ func CallPrivateRPC(inputJSON string) string {
// just modified to handle the function arg passing. // just modified to handle the function arg passing.
func CreateAccount(password string) string { func CreateAccount(password string) string {
info, mnemonic, err := statusBackend.AccountManager().CreateAccount(password) info, mnemonic, err := statusBackend.AccountManager().CreateAccount(password)
errString := "" errString := ""
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) 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, // 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, // to verify ownership if verified, purges all the previous identities from Whisper,
// and injects verified key as shh identity. // and injects verified key as shh identity.
func Login(loginParamsJSON string) string { func Login(accountData, password string) string {
params, err := account.ParseLoginParams(loginParamsJSON) var account multiaccounts.Account
err := json.Unmarshal([]byte(accountData), &account)
if err != nil { 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) return makeJSONResponse(err)
} }
@ -346,7 +386,11 @@ func LoginWithKeycard(chatKeyData, encryptionKeyData string) string {
// Logout is equivalent to clearing whisper identities. // Logout is equivalent to clearing whisper identities.
func Logout() string { func Logout() string {
err := statusBackend.Logout() 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 // SignMessage unmarshals rpc params {data, address, password} and

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

77
multiaccounts/database.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,9 @@ const (
// EventChainDataRemoved is triggered when node's chain data is removed // EventChainDataRemoved is triggered when node's chain data is removed
EventChainDataRemoved = "chaindata.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 // NodeCrashEvent is special kind of error, used to report node crashes
@ -53,3 +56,7 @@ func SendNodeStopped() {
func SendChainDataRemoved() { func SendChainDataRemoved() {
send(EventChainDataRemoved, nil) send(EventChainDataRemoved, nil)
} }
func SendLoggedIn() {
send(EventLoggedIn, nil)
}

39
sqlite/fields.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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