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:
parent
4f1a3283e6
commit
be9c55bc16
1
Makefile
1
Makefile
|
@ -236,7 +236,6 @@ mock-install: ##@other Install mocking tools
|
|||
mock: ##@other Regenerate mocks
|
||||
mockgen -package=fcm -destination=notifications/push/fcm/client_mock.go -source=notifications/push/fcm/client.go
|
||||
mockgen -package=fake -destination=transactions/fake/mock.go -source=transactions/fake/txservice.go
|
||||
mockgen -package=account -destination=account/accounts_mock.go -source=account/accounts.go
|
||||
mockgen -package=status -destination=services/status/account_mock.go -source=services/status/service.go
|
||||
mockgen -package=peer -destination=services/peer/discoverer_mock.go -source=services/peer/service.go
|
||||
|
||||
|
|
|
@ -30,21 +30,16 @@ var (
|
|||
ErrInvalidMasterKeyCreated = errors.New("can not create master extended key")
|
||||
ErrOnboardingNotStarted = errors.New("onboarding must be started before choosing an account")
|
||||
ErrOnboardingAccountNotFound = errors.New("cannot find onboarding account with the given id")
|
||||
ErrAccountKeyStoreMissing = errors.New("account key store is not set")
|
||||
)
|
||||
|
||||
var zeroAddress = common.Address{}
|
||||
|
||||
// GethServiceProvider provides required geth services.
|
||||
type GethServiceProvider interface {
|
||||
AccountManager() (*accounts.Manager, error)
|
||||
AccountKeyStore() (*keystore.KeyStore, error)
|
||||
}
|
||||
|
||||
// Manager represents account manager interface.
|
||||
type Manager struct {
|
||||
geth GethServiceProvider
|
||||
|
||||
mu sync.RWMutex
|
||||
mu sync.RWMutex
|
||||
keystore *keystore.KeyStore
|
||||
manager *accounts.Manager
|
||||
|
||||
accountsGenerator *generator.Generator
|
||||
onboarding *Onboarding
|
||||
|
@ -55,15 +50,43 @@ type Manager struct {
|
|||
}
|
||||
|
||||
// NewManager returns new node account manager.
|
||||
func NewManager(geth GethServiceProvider) *Manager {
|
||||
manager := &Manager{
|
||||
geth: geth,
|
||||
func NewManager() *Manager {
|
||||
m := &Manager{}
|
||||
m.accountsGenerator = generator.New(m)
|
||||
return m
|
||||
}
|
||||
|
||||
// InitKeystore sets key manager and key store.
|
||||
func (m *Manager) InitKeystore(keydir string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
manager, err := makeAccountManager(keydir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.manager = manager
|
||||
backends := manager.Backends(keystore.KeyStoreType)
|
||||
if len(backends) == 0 {
|
||||
return ErrAccountKeyStoreMissing
|
||||
}
|
||||
keyStore, ok := backends[0].(*keystore.KeyStore)
|
||||
if !ok {
|
||||
return ErrAccountKeyStoreMissing
|
||||
}
|
||||
m.keystore = keyStore
|
||||
return nil
|
||||
}
|
||||
|
||||
accountsGenerator := generator.New(manager)
|
||||
manager.accountsGenerator = accountsGenerator
|
||||
func (m *Manager) GetKeystore() *keystore.KeyStore {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.keystore
|
||||
}
|
||||
|
||||
return manager
|
||||
func (m *Manager) GetManager() *accounts.Manager {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.manager
|
||||
}
|
||||
|
||||
// AccountsGenerator returns accountsGenerator.
|
||||
|
@ -270,24 +293,22 @@ func (m *Manager) Logout() {
|
|||
|
||||
// ImportAccount imports the account specified with privateKey.
|
||||
func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error) {
|
||||
keyStore, err := m.geth.AccountKeyStore()
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
if m.keystore == nil {
|
||||
return common.Address{}, ErrAccountKeyStoreMissing
|
||||
}
|
||||
|
||||
account, err := keyStore.ImportECDSA(privateKey, password)
|
||||
account, err := m.keystore.ImportECDSA(privateKey, password)
|
||||
|
||||
return account.Address, err
|
||||
}
|
||||
|
||||
func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
|
||||
keyStore, err := m.geth.AccountKeyStore()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
if m.keystore == nil {
|
||||
return "", "", ErrAccountKeyStoreMissing
|
||||
}
|
||||
|
||||
// imports extended key, create key file (if necessary)
|
||||
account, err := keyStore.ImportSingleExtendedKey(extKey, password)
|
||||
account, err := m.keystore.ImportSingleExtendedKey(extKey, password)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
@ -295,7 +316,7 @@ func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password
|
|||
address = account.Address.Hex()
|
||||
|
||||
// obtain public key to return
|
||||
account, key, err := keyStore.AccountDecryptedKey(account, password)
|
||||
account, key, err := m.keystore.AccountDecryptedKey(account, password)
|
||||
if err != nil {
|
||||
return address, "", err
|
||||
}
|
||||
|
@ -308,20 +329,19 @@ func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password
|
|||
// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key.
|
||||
// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file.
|
||||
func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
|
||||
keyStore, err := m.geth.AccountKeyStore()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
if m.keystore == nil {
|
||||
return "", "", ErrAccountKeyStoreMissing
|
||||
}
|
||||
|
||||
// imports extended key, create key file (if necessary)
|
||||
account, err := keyStore.ImportExtendedKeyForPurpose(keyPurpose, extKey, password)
|
||||
account, err := m.keystore.ImportExtendedKeyForPurpose(keyPurpose, extKey, password)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
address = account.Address.Hex()
|
||||
|
||||
// obtain public key to return
|
||||
account, key, err := keyStore.AccountDecryptedKey(account, password)
|
||||
account, key, err := m.keystore.AccountDecryptedKey(account, password)
|
||||
if err != nil {
|
||||
return address, "", err
|
||||
}
|
||||
|
@ -335,7 +355,6 @@ func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extke
|
|||
func (m *Manager) Accounts() ([]gethcommon.Address, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
addresses := make([]gethcommon.Address, 0)
|
||||
if m.mainAccountAddress != zeroAddress {
|
||||
addresses = append(addresses, m.mainAccountAddress)
|
||||
|
@ -397,9 +416,8 @@ func (m *Manager) ImportOnboardingAccount(id string, password string) (Info, str
|
|||
// The running node, has a keystore directory which is loaded on start. Key file
|
||||
// for a given address is expected to be in that directory prior to node start.
|
||||
func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) {
|
||||
keyStore, err := m.geth.AccountKeyStore()
|
||||
if err != nil {
|
||||
return accounts.Account{}, nil, err
|
||||
if m.keystore == nil {
|
||||
return accounts.Account{}, nil, ErrAccountKeyStoreMissing
|
||||
}
|
||||
|
||||
account, err := ParseAccountString(address)
|
||||
|
@ -408,7 +426,7 @@ func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.
|
|||
}
|
||||
|
||||
var key *keystore.Key
|
||||
account, key, err = keyStore.AccountDecryptedKey(account, password)
|
||||
account, key, err = m.keystore.AccountDecryptedKey(account, password)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -9,20 +9,16 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/golang/mock/gomock"
|
||||
. "github.com/status-im/status-go/t/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestVerifyAccountPassword(t *testing.T) {
|
||||
accManager := NewManager(nil)
|
||||
accManager := NewManager()
|
||||
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(keyStoreDir) //nolint: errcheck
|
||||
|
@ -108,70 +104,22 @@ func TestVerifyAccountPasswordWithAccountBeforeEIP55(t *testing.T) {
|
|||
err = ImportTestAccount(keyStoreDir, "test-account3-before-eip55.pk")
|
||||
require.NoError(t, err)
|
||||
|
||||
accManager := NewManager(nil)
|
||||
accManager := NewManager()
|
||||
|
||||
address := gethcommon.HexToAddress(TestConfig.Account3.WalletAddress)
|
||||
_, err = accManager.VerifyAccountPassword(keyStoreDir, address.Hex(), TestConfig.Account3.Password)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
var errKeyStore = errors.New("Can't return a key store")
|
||||
|
||||
func TestManagerTestSuite(t *testing.T) {
|
||||
gethServiceProvider := newMockGethServiceProvider(t)
|
||||
accManager := NewManager(gethServiceProvider)
|
||||
|
||||
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
|
||||
require.NoError(t, err)
|
||||
keyStore := keystore.NewKeyStore(keyStoreDir, keystore.LightScryptN, keystore.LightScryptP)
|
||||
defer os.RemoveAll(keyStoreDir) //nolint: errcheck
|
||||
|
||||
testPassword := "test-password"
|
||||
|
||||
// Initial test - create test account
|
||||
gethServiceProvider.EXPECT().AccountKeyStore().Return(keyStore, nil)
|
||||
accountInfo, mnemonic, err := accManager.CreateAccount(testPassword)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, accountInfo.WalletAddress)
|
||||
require.NotEmpty(t, accountInfo.WalletPubKey)
|
||||
require.NotEmpty(t, accountInfo.ChatAddress)
|
||||
require.NotEmpty(t, accountInfo.ChatPubKey)
|
||||
require.NotEmpty(t, mnemonic)
|
||||
|
||||
// Before the complete decoupling of the keys, wallet and chat keys are the same
|
||||
assert.Equal(t, accountInfo.WalletAddress, accountInfo.ChatAddress)
|
||||
assert.Equal(t, accountInfo.WalletPubKey, accountInfo.ChatPubKey)
|
||||
|
||||
s := &ManagerTestSuite{
|
||||
testAccount: testAccount{
|
||||
"test-password",
|
||||
accountInfo.WalletAddress,
|
||||
accountInfo.WalletPubKey,
|
||||
accountInfo.ChatAddress,
|
||||
accountInfo.ChatPubKey,
|
||||
mnemonic,
|
||||
},
|
||||
gethServiceProvider: gethServiceProvider,
|
||||
accManager: accManager,
|
||||
keyStore: keyStore,
|
||||
gethAccManager: accounts.NewManager(),
|
||||
}
|
||||
|
||||
suite.Run(t, s)
|
||||
}
|
||||
|
||||
func newMockGethServiceProvider(t *testing.T) *MockGethServiceProvider {
|
||||
ctrl := gomock.NewController(t)
|
||||
return NewMockGethServiceProvider(ctrl)
|
||||
suite.Run(t, new(ManagerTestSuite))
|
||||
}
|
||||
|
||||
type ManagerTestSuite struct {
|
||||
suite.Suite
|
||||
testAccount
|
||||
gethServiceProvider *MockGethServiceProvider
|
||||
accManager *Manager
|
||||
keyStore *keystore.KeyStore
|
||||
gethAccManager *accounts.Manager
|
||||
accManager *Manager
|
||||
keydir string
|
||||
}
|
||||
|
||||
type testAccount struct {
|
||||
|
@ -183,43 +131,52 @@ type testAccount struct {
|
|||
mnemonic string
|
||||
}
|
||||
|
||||
// reinitMock is for reassigning a new mock node manager to account manager.
|
||||
// Stating the amount of times for mock calls kills the flexibility for
|
||||
// development so this is a good workaround to use with EXPECT().Func().AnyTimes()
|
||||
func (s *ManagerTestSuite) reinitMock() {
|
||||
s.gethServiceProvider = newMockGethServiceProvider(s.T())
|
||||
s.accManager.geth = s.gethServiceProvider
|
||||
}
|
||||
|
||||
// SetupTest is used here for reinitializing the mock before every
|
||||
// test function to avoid faulty execution.
|
||||
func (s *ManagerTestSuite) SetupTest() {
|
||||
s.reinitMock()
|
||||
s.accManager = NewManager()
|
||||
|
||||
keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts")
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(s.accManager.InitKeystore(keyStoreDir))
|
||||
s.keydir = keyStoreDir
|
||||
|
||||
testPassword := "test-password"
|
||||
|
||||
// Initial test - create test account
|
||||
accountInfo, mnemonic, err := s.accManager.CreateAccount(testPassword)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotEmpty(accountInfo.WalletAddress)
|
||||
s.Require().NotEmpty(accountInfo.WalletPubKey)
|
||||
s.Require().NotEmpty(accountInfo.ChatAddress)
|
||||
s.Require().NotEmpty(accountInfo.ChatPubKey)
|
||||
s.Require().NotEmpty(mnemonic)
|
||||
|
||||
// Before the complete decoupling of the keys, wallet and chat keys are the same
|
||||
s.Equal(accountInfo.WalletAddress, accountInfo.ChatAddress)
|
||||
s.Equal(accountInfo.WalletPubKey, accountInfo.ChatPubKey)
|
||||
|
||||
s.testAccount = testAccount{
|
||||
testPassword,
|
||||
accountInfo.WalletAddress,
|
||||
accountInfo.WalletPubKey,
|
||||
accountInfo.ChatAddress,
|
||||
accountInfo.ChatPubKey,
|
||||
mnemonic,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestCreateAccount() {
|
||||
// Don't fail on empty password
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
|
||||
_, _, err := s.accManager.CreateAccount(s.password)
|
||||
s.NoError(err)
|
||||
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(nil, errKeyStore)
|
||||
_, _, err = s.accManager.CreateAccount(s.password)
|
||||
s.Equal(errKeyStore, err)
|
||||
func (s *ManagerTestSuite) TearDownTest() {
|
||||
s.Require().NoError(os.RemoveAll(s.keydir))
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestRecoverAccount() {
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
|
||||
accountInfo, err := s.accManager.RecoverAccount(s.password, s.mnemonic)
|
||||
s.NoError(err)
|
||||
s.Equal(s.walletAddress, accountInfo.WalletAddress)
|
||||
s.Equal(s.walletPubKey, accountInfo.WalletPubKey)
|
||||
s.Equal(s.chatAddress, accountInfo.ChatAddress)
|
||||
s.Equal(s.chatPubKey, accountInfo.ChatPubKey)
|
||||
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(nil, errKeyStore)
|
||||
_, err = s.accManager.RecoverAccount(s.password, s.mnemonic)
|
||||
s.Equal(errKeyStore, err)
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestOnboarding() {
|
||||
|
@ -240,7 +197,6 @@ func (s *ManagerTestSuite) TestOnboarding() {
|
|||
// choose one account and encrypt it with password
|
||||
password := "test-onboarding-account"
|
||||
account := accounts[0]
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
|
||||
info, mnemonic, err := s.accManager.ImportOnboardingAccount(account.ID, password)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(account.Info, info)
|
||||
|
@ -248,7 +204,6 @@ func (s *ManagerTestSuite) TestOnboarding() {
|
|||
s.Nil(s.accManager.onboarding)
|
||||
|
||||
// try to decrypt it with password to check if it's been imported correctly
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
|
||||
decAccount, _, err := s.accManager.AddressToDecryptedAccount(info.WalletAddress, password)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(info.WalletAddress, decAccount.Address.Hex())
|
||||
|
@ -262,79 +217,43 @@ func (s *ManagerTestSuite) TestOnboarding() {
|
|||
s.Nil(s.accManager.onboarding)
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestSelectAccount() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
accountKeyStoreReturn []interface{}
|
||||
walletAddress string
|
||||
chatAddress string
|
||||
password string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
"success",
|
||||
[]interface{}{s.keyStore, nil},
|
||||
s.walletAddress,
|
||||
s.chatAddress,
|
||||
s.password,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"fail_keyStore",
|
||||
[]interface{}{nil, errKeyStore},
|
||||
s.walletAddress,
|
||||
s.chatAddress,
|
||||
s.password,
|
||||
errKeyStore,
|
||||
},
|
||||
{
|
||||
"fail_wrongChatAddress",
|
||||
[]interface{}{s.keyStore, nil},
|
||||
s.walletAddress,
|
||||
"0x0000000000000000000000000000000000000001",
|
||||
s.password,
|
||||
errors.New("cannot retrieve a valid key for a given account: no key for given address or file"),
|
||||
},
|
||||
{
|
||||
"fail_wrongPassword",
|
||||
[]interface{}{s.keyStore, nil},
|
||||
s.walletAddress,
|
||||
s.chatAddress,
|
||||
"wrong-password",
|
||||
errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"),
|
||||
},
|
||||
func (s *ManagerTestSuite) TestSelectAccountSuccess() {
|
||||
s.testSelectAccount(common.HexToAddress(s.testAccount.chatAddress), common.HexToAddress(s.testAccount.walletAddress), s.testAccount.password, nil)
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestSelectAccountWrongAddress() {
|
||||
s.testSelectAccount(common.HexToAddress("0x0000000000000000000000000000000000000001"), common.HexToAddress(s.testAccount.walletAddress), s.testAccount.password, errors.New("cannot retrieve a valid key for a given account: no key for given address or file"))
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestSelectAccountWrongPassword() {
|
||||
s.testSelectAccount(common.HexToAddress(s.testAccount.chatAddress), common.HexToAddress(s.testAccount.walletAddress), "wrong", errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"))
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) testSelectAccount(chat, wallet common.Address, password string, expErr error) {
|
||||
loginParams := LoginParams{
|
||||
ChatAddress: chat,
|
||||
MainAccount: wallet,
|
||||
Password: password,
|
||||
}
|
||||
err := s.accManager.SelectAccount(loginParams)
|
||||
s.Require().Equal(expErr, err)
|
||||
|
||||
selectedMainAccountAddress, walletErr := s.accManager.MainAccountAddress()
|
||||
selectedChatAccount, chatErr := s.accManager.SelectedChatAccount()
|
||||
|
||||
if expErr == nil {
|
||||
s.Require().NoError(walletErr)
|
||||
s.Require().NoError(chatErr)
|
||||
s.Equal(wallet, selectedMainAccountAddress)
|
||||
s.Equal(chat, crypto.PubkeyToAddress(selectedChatAccount.AccountKey.PrivateKey.PublicKey))
|
||||
} else {
|
||||
s.Equal(common.Address{}, selectedMainAccountAddress)
|
||||
s.Nil(selectedChatAccount)
|
||||
s.Equal(walletErr, ErrNoAccountSelected)
|
||||
s.Equal(chatErr, ErrNoAccountSelected)
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
s.T().Run(testCase.name, func(t *testing.T) {
|
||||
s.reinitMock()
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(testCase.accountKeyStoreReturn...).AnyTimes()
|
||||
loginParams := LoginParams{
|
||||
ChatAddress: common.HexToAddress(testCase.chatAddress),
|
||||
MainAccount: common.HexToAddress(testCase.walletAddress),
|
||||
Password: testCase.password,
|
||||
}
|
||||
err := s.accManager.SelectAccount(loginParams)
|
||||
s.Equal(testCase.expectedError, err)
|
||||
|
||||
selectedMainAccountAddress, walletErr := s.accManager.MainAccountAddress()
|
||||
selectedChatAccount, chatErr := s.accManager.SelectedChatAccount()
|
||||
|
||||
if testCase.expectedError == nil {
|
||||
s.Equal(testCase.walletAddress, selectedMainAccountAddress.String())
|
||||
s.Equal(testCase.chatAddress, crypto.PubkeyToAddress(selectedChatAccount.AccountKey.PrivateKey.PublicKey).Hex())
|
||||
s.NoError(walletErr)
|
||||
s.NoError(chatErr)
|
||||
} else {
|
||||
s.Equal(common.Address{}, selectedMainAccountAddress)
|
||||
s.Nil(selectedChatAccount)
|
||||
s.Equal(walletErr, ErrNoAccountSelected)
|
||||
s.Equal(chatErr, ErrNoAccountSelected)
|
||||
}
|
||||
|
||||
s.accManager.Logout()
|
||||
})
|
||||
}
|
||||
s.accManager.Logout()
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestSetChatAccount() {
|
||||
|
@ -367,7 +286,6 @@ func (s *ManagerTestSuite) TestLogout() {
|
|||
// TestAccounts tests cases for (*Manager).Accounts.
|
||||
func (s *ManagerTestSuite) TestAccounts() {
|
||||
// Select the test account
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil).AnyTimes()
|
||||
loginParams := LoginParams{
|
||||
MainAccount: common.HexToAddress(s.walletAddress),
|
||||
ChatAddress: common.HexToAddress(s.chatAddress),
|
||||
|
@ -377,70 +295,36 @@ func (s *ManagerTestSuite) TestAccounts() {
|
|||
s.NoError(err)
|
||||
|
||||
// Success
|
||||
s.gethServiceProvider.EXPECT().AccountManager().Return(s.gethAccManager, nil)
|
||||
accs, err := s.accManager.Accounts()
|
||||
s.NoError(err)
|
||||
s.NotNil(accs)
|
||||
|
||||
// Selected main account address is zero address but doesn't fail
|
||||
s.accManager.mainAccountAddress = common.Address{}
|
||||
s.gethServiceProvider.EXPECT().AccountManager().Return(s.gethAccManager, nil)
|
||||
accs, err = s.accManager.Accounts()
|
||||
s.NoError(err)
|
||||
s.NotNil(accs)
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestAddressToDecryptedAccount() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
accountKeyStoreReturn []interface{}
|
||||
walletAddress string
|
||||
password string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
"success",
|
||||
[]interface{}{s.keyStore, nil},
|
||||
s.walletAddress,
|
||||
s.password,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"fail_keyStore",
|
||||
[]interface{}{nil, errKeyStore},
|
||||
s.walletAddress,
|
||||
s.password,
|
||||
errKeyStore,
|
||||
},
|
||||
{
|
||||
"fail_wrongWalletAddress",
|
||||
[]interface{}{s.keyStore, nil},
|
||||
"wrong-wallet-address",
|
||||
s.password,
|
||||
ErrAddressToAccountMappingFailure,
|
||||
},
|
||||
{
|
||||
"fail_wrongPassword",
|
||||
[]interface{}{s.keyStore, nil},
|
||||
s.walletAddress,
|
||||
"wrong-password",
|
||||
errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"),
|
||||
},
|
||||
}
|
||||
func (s *ManagerTestSuite) TestAddressToDecryptedAccountSuccess() {
|
||||
s.testAddressToDecryptedAccount(s.walletAddress, s.password, nil)
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
s.T().Run(testCase.name, func(t *testing.T) {
|
||||
s.reinitMock()
|
||||
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(testCase.accountKeyStoreReturn...).AnyTimes()
|
||||
acc, key, err := s.accManager.AddressToDecryptedAccount(testCase.walletAddress, testCase.password)
|
||||
if testCase.expectedError != nil {
|
||||
s.Equal(testCase.expectedError, err)
|
||||
} else {
|
||||
s.NoError(err)
|
||||
s.NotNil(acc)
|
||||
s.NotNil(key)
|
||||
s.Equal(acc.Address, key.Address)
|
||||
}
|
||||
})
|
||||
func (s *ManagerTestSuite) TestAddressToDecryptedAccountWrongAddress() {
|
||||
s.testAddressToDecryptedAccount("0x0001", s.password, ErrAddressToAccountMappingFailure)
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) TestAddressToDecryptedAccountWrongPassword() {
|
||||
s.testAddressToDecryptedAccount(s.walletAddress, "wrong", errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given passphrase"))
|
||||
}
|
||||
|
||||
func (s *ManagerTestSuite) testAddressToDecryptedAccount(wallet, password string, expErr error) {
|
||||
acc, key, err := s.accManager.AddressToDecryptedAccount(wallet, password)
|
||||
if expErr != nil {
|
||||
s.Equal(expErr, err)
|
||||
} else {
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(acc)
|
||||
s.Require().NotNil(key)
|
||||
s.Equal(acc.Address, key.Address)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -42,7 +42,6 @@ func ParseLoginParams(paramsJSON string) (LoginParams, error) {
|
|||
params LoginParams
|
||||
zeroAddress common.Address
|
||||
)
|
||||
|
||||
if err := json.Unmarshal([]byte(paramsJSON), ¶ms); err != nil {
|
||||
return params, err
|
||||
}
|
||||
|
@ -60,7 +59,6 @@ func ParseLoginParams(paramsJSON string) (LoginParams, error) {
|
|||
return params, newErrZeroAddress("WatchAddresses")
|
||||
}
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
|
|
194
api/backend.go
194
api/backend.go
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"math/big"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -19,11 +20,15 @@ import (
|
|||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/crypto"
|
||||
"github.com/status-im/status-go/logutils"
|
||||
"github.com/status-im/status-go/mailserver/registry"
|
||||
"github.com/status-im/status-go/multiaccounts"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/node"
|
||||
"github.com/status-im/status-go/notifications/push/fcm"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
accountssvc "github.com/status-im/status-go/services/accounts"
|
||||
"github.com/status-im/status-go/services/personal"
|
||||
"github.com/status-im/status-go/services/rpcfilters"
|
||||
"github.com/status-im/status-go/services/subscriptions"
|
||||
|
@ -52,10 +57,14 @@ var (
|
|||
|
||||
// StatusBackend implements Status.im service
|
||||
type StatusBackend struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
// rootDataDir is the same for all networks.
|
||||
rootDataDir string
|
||||
accountsDB *accounts.Database
|
||||
statusNode *node.StatusNode
|
||||
personalAPI *personal.PublicAPI
|
||||
rpcFilters *rpcfilters.Service
|
||||
multiaccountsDB *multiaccounts.Database
|
||||
accountManager *account.Manager
|
||||
transactor *transactions.Transactor
|
||||
newNotification fcm.NotificationConstructor
|
||||
|
@ -70,12 +79,11 @@ func NewStatusBackend() *StatusBackend {
|
|||
defer log.Info("Status backend initialized", "version", params.Version, "commit", params.GitCommit)
|
||||
|
||||
statusNode := node.New()
|
||||
accountManager := account.NewManager(statusNode)
|
||||
accountManager := account.NewManager()
|
||||
transactor := transactions.NewTransactor()
|
||||
personalAPI := personal.NewAPI()
|
||||
notificationManager := fcm.NewNotification(fcmServerKey)
|
||||
rpcFilters := rpcfilters.New(statusNode)
|
||||
|
||||
return &StatusBackend{
|
||||
statusNode: statusNode,
|
||||
accountManager: accountManager,
|
||||
|
@ -121,6 +129,144 @@ func (b *StatusBackend) StartNode(config *params.NodeConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (b *StatusBackend) UpdateRootDataDir(datadir string) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.rootDataDir = datadir
|
||||
}
|
||||
|
||||
func (b *StatusBackend) OpenAccounts() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.multiaccountsDB != nil {
|
||||
return nil
|
||||
}
|
||||
db, err := multiaccounts.InitializeDB(filepath.Join(b.rootDataDir, "accounts.sql"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.multiaccountsDB = db
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *StatusBackend) GetAccounts() ([]multiaccounts.Account, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.multiaccountsDB == nil {
|
||||
return nil, errors.New("accoutns db wasn't initialized")
|
||||
}
|
||||
return b.multiaccountsDB.GetAccounts()
|
||||
}
|
||||
|
||||
func (b *StatusBackend) SaveAccount(account multiaccounts.Account) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.multiaccountsDB == nil {
|
||||
return errors.New("accoutns db wasn't initialized")
|
||||
}
|
||||
return b.multiaccountsDB.SaveAccount(account)
|
||||
}
|
||||
|
||||
func (b *StatusBackend) ensureAccountsDBOpened(account multiaccounts.Account, password string) (err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.accountsDB != nil {
|
||||
return nil
|
||||
}
|
||||
if len(b.rootDataDir) == 0 {
|
||||
return errors.New("root datadir wasn't provided")
|
||||
}
|
||||
path := filepath.Join(b.rootDataDir, fmt.Sprintf("accounts-%x.sql", account.Address))
|
||||
b.accountsDB, err = accounts.InitializeDB(path, password)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *StatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string) error {
|
||||
err := b.ensureAccountsDBOpened(acc, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf, err := b.loadNodeConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := logutils.OverrideRootLogWithConfig(conf, false); err != nil {
|
||||
return err
|
||||
}
|
||||
chatAddr, err := b.accountsDB.GetChatAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
walletAddr, err := b.accountsDB.GetWalletAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
watchAddrs, err := b.accountsDB.GetAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
login := account.LoginParams{
|
||||
Password: password,
|
||||
ChatAddress: chatAddr,
|
||||
WatchAddresses: watchAddrs,
|
||||
MainAccount: walletAddr,
|
||||
}
|
||||
err = b.StartNode(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.SelectAccount(login)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.Address, time.Now().Unix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signal.SendLoggedIn()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartNodeWithAccountAndConfig is used after account and config was generated.
|
||||
// In current setup account name and config is generated on the client side. Once/if it will be generated on
|
||||
// status-go side this flow can be simplified.
|
||||
func (b *StatusBackend) StartNodeWithAccountAndConfig(account multiaccounts.Account, password string, conf *params.NodeConfig, subaccs []accounts.Account) error {
|
||||
err := b.SaveAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.ensureAccountsDBOpened(account, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.saveNodeConfig(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.accountsDB.SaveAccounts(subaccs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.StartNodeWithAccount(account, password)
|
||||
}
|
||||
|
||||
func (b *StatusBackend) saveNodeConfig(config *params.NodeConfig) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.accountsDB.SaveConfig(accounts.NodeConfigTag, config)
|
||||
}
|
||||
|
||||
func (b *StatusBackend) loadNodeConfig() (*params.NodeConfig, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
conf := params.NodeConfig{}
|
||||
err := b.accountsDB.GetConfig(accounts.NodeConfigTag, &conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &conf, nil
|
||||
}
|
||||
|
||||
func (b *StatusBackend) rpcFiltersService() gethnode.ServiceConstructor {
|
||||
return func(*gethnode.ServiceContext) (gethnode.Service, error) {
|
||||
return rpcfilters.New(b.statusNode), nil
|
||||
|
@ -133,6 +279,12 @@ func (b *StatusBackend) subscriptionService() gethnode.ServiceConstructor {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *StatusBackend) accountsService() gethnode.ServiceConstructor {
|
||||
return func(*gethnode.ServiceContext) (gethnode.Service, error) {
|
||||
return accountssvc.NewService(b.accountsDB, b.multiaccountsDB, b.AccountManager()), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
@ -144,17 +296,22 @@ func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
|
|||
if err := config.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services := []gethnode.ServiceConstructor{}
|
||||
services = appendIf(config.UpstreamConfig.Enabled, services, b.rpcFiltersService())
|
||||
services = append(services, b.subscriptionService())
|
||||
services = appendIf(b.accountsDB != nil, services, b.accountsService())
|
||||
|
||||
manager := b.accountManager.GetManager()
|
||||
if manager == nil {
|
||||
return errors.New("ethereum accounts.Manager is nil")
|
||||
}
|
||||
if err = b.statusNode.StartWithOptions(config, node.StartOptions{
|
||||
Services: services,
|
||||
// The peers discovery protocols are started manually after
|
||||
// `node.ready` signal is sent.
|
||||
// It was discussed in https://github.com/status-im/status-go/pull/1333.
|
||||
StartDiscovery: false,
|
||||
StartDiscovery: false,
|
||||
AccountsManager: manager,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -484,6 +641,22 @@ func (b *StatusBackend) Logout() error {
|
|||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
err := b.cleanupServices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.stopAccountsDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AccountManager().Logout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupServices stops parts of services that doesn't managed by a node and removes injected data from services.
|
||||
func (b *StatusBackend) cleanupServices() error {
|
||||
whisperService, err := b.statusNode.WhisperService()
|
||||
switch err {
|
||||
case node.ErrServiceUnknown: // Whisper was never registered
|
||||
|
@ -521,9 +694,15 @@ func (b *StatusBackend) Logout() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
b.AccountManager().Logout()
|
||||
|
||||
func (b *StatusBackend) stopAccountsDB() error {
|
||||
if b.accountsDB != nil {
|
||||
err := b.accountsDB.Close()
|
||||
b.accountsDB = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -622,7 +801,6 @@ func (b *StatusBackend) startWallet(password string) error {
|
|||
allAddresses := make([]common.Address, len(watchAddresses)+1)
|
||||
allAddresses[0] = mainAccountAddress
|
||||
copy(allAddresses[1:], watchAddresses)
|
||||
|
||||
path := path.Join(b.statusNode.Config().DataDir, fmt.Sprintf("wallet-%x.sql", mainAccountAddress))
|
||||
return wallet.StartReactor(path, password,
|
||||
b.statusNode.RPCClient().Ethclient(),
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/node"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/signal"
|
||||
"github.com/status-im/status-go/t/utils"
|
||||
|
@ -23,11 +24,16 @@ const (
|
|||
func TestSubscriptionEthWithParamsDict(t *testing.T) {
|
||||
// a simple test to check the parameter parsing for eth_* filter subscriptions
|
||||
backend := NewStatusBackend()
|
||||
// initNodeAndLogin can fail and terminate the test, in that case stopNode must be executed anyway.
|
||||
defer func() {
|
||||
err := backend.StopNode()
|
||||
if err != node.ErrNoRunningNode {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
initNodeAndLogin(t, backend)
|
||||
|
||||
defer func() { require.NoError(t, backend.StopNode()) }()
|
||||
|
||||
createSubscription(t, backend, fmt.Sprintf(`"eth_newFilter", [
|
||||
{
|
||||
"fromBlock":"earliest",
|
||||
|
@ -40,11 +46,15 @@ func TestSubscriptionEthWithParamsDict(t *testing.T) {
|
|||
func TestSubscriptionPendingTransaction(t *testing.T) {
|
||||
backend := NewStatusBackend()
|
||||
backend.allowAllRPC = true
|
||||
defer func() {
|
||||
err := backend.StopNode()
|
||||
if err != node.ErrNoRunningNode {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
account, _ := initNodeAndLogin(t, backend)
|
||||
|
||||
defer func() { require.NoError(t, backend.StopNode()) }()
|
||||
|
||||
signals := make(chan string)
|
||||
defer func() {
|
||||
signal.ResetDefaultNodeNotificationHandler()
|
||||
|
@ -88,11 +98,15 @@ func TestSubscriptionPendingTransaction(t *testing.T) {
|
|||
|
||||
func TestSubscriptionWhisperEnvelopes(t *testing.T) {
|
||||
backend := NewStatusBackend()
|
||||
defer func() {
|
||||
err := backend.StopNode()
|
||||
if err != node.ErrNoRunningNode {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
initNodeAndLogin(t, backend)
|
||||
|
||||
defer func() { require.NoError(t, backend.StopNode()) }()
|
||||
|
||||
signals := make(chan string)
|
||||
defer func() {
|
||||
signal.ResetDefaultNodeNotificationHandler()
|
||||
|
@ -222,9 +236,9 @@ func initNodeAndLogin(t *testing.T, backend *StatusBackend) (string, string) {
|
|||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, _, err := backend.AccountManager().CreateAccount(password)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ func TestBackendStartNodeConcurrently(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
count := 2
|
||||
resultCh := make(chan error)
|
||||
|
||||
|
@ -58,9 +59,8 @@ func TestBackendRestartNodeConcurrently(t *testing.T) {
|
|||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
count := 3
|
||||
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
require.NoError(t, backend.StartNode(config))
|
||||
defer func() {
|
||||
require.NoError(t, backend.StopNode())
|
||||
}()
|
||||
|
@ -84,7 +84,7 @@ func TestBackendGettersConcurrently(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
|
@ -136,7 +136,7 @@ func TestBackendAccountsConcurrently(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
|
@ -196,6 +196,7 @@ func TestBackendInjectChatAccount(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
|
@ -269,6 +270,7 @@ func TestBackendCallRPCConcurrently(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
count := 3
|
||||
|
||||
err = backend.StartNode(config)
|
||||
|
@ -342,6 +344,7 @@ func TestBlockedRPCMethods(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, backend.StopNode()) }()
|
||||
|
@ -379,6 +382,7 @@ func TestStartStopMultipleTimes(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
config.NoDiscovery = false
|
||||
// doesn't have to be running. just any valid enode to bypass validation.
|
||||
config.ClusterConfig.BootNodes = []string{
|
||||
|
@ -395,6 +399,7 @@ func TestSignHash(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
|
||||
require.NoError(t, backend.StartNode(config))
|
||||
defer func() {
|
||||
|
@ -432,6 +437,7 @@ func TestHashTypedData(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
|
|
|
@ -16,6 +16,7 @@ func TestHashMessage(t *testing.T) {
|
|||
backend := NewStatusBackend()
|
||||
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
err = backend.StartNode(config)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
|
|
102
lib/library.go
102
lib/library.go
|
@ -12,10 +12,11 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/api"
|
||||
"github.com/status-im/status-go/exportlogs"
|
||||
"github.com/status-im/status-go/logutils"
|
||||
"github.com/status-im/status-go/multiaccounts"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/profiling"
|
||||
"github.com/status-im/status-go/services/personal"
|
||||
|
@ -28,28 +29,23 @@ import (
|
|||
// All general log messages in this package should be routed through this logger.
|
||||
var logger = log.New("package", "status-go/lib")
|
||||
|
||||
//StartNode - start Status node
|
||||
//export StartNode
|
||||
func StartNode(configJSON *C.char) *C.char {
|
||||
config, err := params.NewConfigFromJSON(C.GoString(configJSON))
|
||||
// OpenAccounts opens database and returns accounts list.
|
||||
//export OpenAccounts
|
||||
func OpenAccounts(datadir *C.char) *C.char {
|
||||
statusBackend.UpdateRootDataDir(C.GoString(datadir))
|
||||
err := statusBackend.OpenAccounts()
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
if err := logutils.OverrideRootLogWithConfig(config, false); err != nil {
|
||||
accs, err := statusBackend.GetAccounts()
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
api.RunAsync(func() error { return statusBackend.StartNode(config) })
|
||||
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
//StopNode - stop status node
|
||||
//export StopNode
|
||||
func StopNode() *C.char {
|
||||
api.RunAsync(statusBackend.StopNode)
|
||||
return makeJSONResponse(nil)
|
||||
data, err := json.Marshal(accs)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature
|
||||
|
@ -324,16 +320,72 @@ func VerifyAccountPassword(keyStoreDir, address, password *C.char) *C.char {
|
|||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
//StartNode - start Status node
|
||||
//export StartNode
|
||||
func StartNode(configJSON *C.char) *C.char {
|
||||
config, err := params.NewConfigFromJSON(C.GoString(configJSON))
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
if err := logutils.OverrideRootLogWithConfig(config, false); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
api.RunAsync(func() error { return statusBackend.StartNode(config) })
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
//StopNode - stop status node
|
||||
//export StopNode
|
||||
func StopNode() *C.char {
|
||||
api.RunAsync(statusBackend.StopNode)
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
//Login loads a key file (for a given address), tries to decrypt it using the password, to verify ownership
|
||||
// if verified, purges all the previous identities from Whisper, and injects verified key as shh identity
|
||||
//export Login
|
||||
func Login(loginParamsJSON *C.char) *C.char {
|
||||
params, err := account.ParseLoginParams(C.GoString(loginParamsJSON))
|
||||
func Login(accountData, password *C.char) *C.char {
|
||||
data, pass := C.GoString(accountData), C.GoString(password)
|
||||
var account multiaccounts.Account
|
||||
err := json.Unmarshal([]byte(data), &account)
|
||||
if err != nil {
|
||||
return C.CString(prepareJSONResponseWithCode(nil, err, codeFailedParseParams))
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
api.RunAsync(func() error { return statusBackend.StartNodeWithAccount(account, pass) })
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
err = statusBackend.SelectAccount(params)
|
||||
// SaveAccountAndLogin saves account in status-go database..
|
||||
//export SaveAccountAndLogin
|
||||
func SaveAccountAndLogin(accountData, password, configJSON, subaccountData *C.char) *C.char {
|
||||
data, confJSON, subData := C.GoString(accountData), C.GoString(configJSON), C.GoString(subaccountData)
|
||||
var account multiaccounts.Account
|
||||
err := json.Unmarshal([]byte(data), &account)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
conf := params.NodeConfig{}
|
||||
err = json.Unmarshal([]byte(confJSON), &conf)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
var subaccs []accounts.Account
|
||||
err = json.Unmarshal([]byte(subData), &subaccs)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
api.RunAsync(func() error {
|
||||
return statusBackend.StartNodeWithAccountAndConfig(account, C.GoString(password), &conf, subaccs)
|
||||
})
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
// InitKeystore initialize keystore before doing any operations with keys.
|
||||
//export InitKeystore
|
||||
func InitKeystore(keydir *C.char) *C.char {
|
||||
err := statusBackend.AccountManager().InitKeystore(C.GoString(keydir))
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
|
@ -349,7 +401,11 @@ func LoginWithKeycard(chatKeyData, encryptionKeyData *C.char) *C.char {
|
|||
//export Logout
|
||||
func Logout() *C.char {
|
||||
err := statusBackend.Logout()
|
||||
return makeJSONResponse(err)
|
||||
if err != nil {
|
||||
makeJSONResponse(err)
|
||||
}
|
||||
api.RunAsync(statusBackend.StopNode)
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
// SignMessage unmarshals rpc params {data, address, password} and passes
|
||||
|
|
|
@ -16,10 +16,7 @@ import (
|
|||
// the actual test functions are in non-_test.go files (so that they can use cgo i.e. import "C")
|
||||
// the only intent of these wrappers is for gotest can find what tests are exposed.
|
||||
func TestExportedAPI(t *testing.T) {
|
||||
allTestsDone := make(chan struct{}, 1)
|
||||
go testExportedAPI(t, allTestsDone)
|
||||
|
||||
<-allTestsDone
|
||||
testExportedAPI(t)
|
||||
}
|
||||
|
||||
func TestValidateNodeConfig(t *testing.T) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
)
|
||||
|
||||
|
@ -50,12 +51,7 @@ func checkMultiAccountResponse(t *testing.T, respJSON *C.char, resp interface{})
|
|||
}
|
||||
}
|
||||
|
||||
func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
|
||||
params := C.CString(`{
|
||||
"n": 2,
|
||||
"mnemonicPhraseLength": 24,
|
||||
|
@ -90,7 +86,6 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
password := "multi-account-test-password"
|
||||
|
||||
// store 2 derived child accounts from the first account.
|
||||
|
@ -115,9 +110,7 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
rawResp = MultiAccountReset()
|
||||
|
||||
// try again deriving addresses.
|
||||
// it should fail because reset should remove all the accounts from memory.
|
||||
for _, loadedID := range loadedIDs {
|
||||
|
@ -126,16 +119,10 @@ func testMultiAccountGenerateDeriveStoreLoadReset(t *testing.T) bool { //nolint:
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func testMultiAccountImportMnemonicAndDerive(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
func testMultiAccountImportMnemonicAndDerive(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
|
||||
mnemonicPhrase := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
bip39Passphrase := "TREZOR"
|
||||
params := mobile.MultiAccountImportMnemonicParams{
|
||||
|
@ -210,7 +197,6 @@ func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []str
|
|||
|
||||
addresses[path] = info.Address
|
||||
}
|
||||
|
||||
return addresses, true
|
||||
}
|
||||
|
||||
|
@ -247,7 +233,8 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, password strin
|
|||
}
|
||||
|
||||
// for each stored account, check that we can decrypt it with the password we used.
|
||||
dir := statusBackend.StatusNode().Config().DataDir
|
||||
// FIXME pass it somehow
|
||||
dir := keystoreDir
|
||||
for _, address := range addresses {
|
||||
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password)
|
||||
if err != nil {
|
||||
|
@ -259,12 +246,7 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, password strin
|
|||
return addresses, true
|
||||
}
|
||||
|
||||
func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
func testMultiAccountGenerateAndDerive(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
|
||||
paths := []string{"m/0", "m/1"}
|
||||
params := mobile.MultiAccountGenerateAndDeriveAddressesParams{
|
||||
MultiAccountGenerateParams: mobile.MultiAccountGenerateParams{
|
||||
|
@ -303,12 +285,7 @@ func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
|
|||
return true
|
||||
}
|
||||
|
||||
func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
func testMultiAccountImportStore(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Errorf("failed generating key")
|
||||
|
@ -350,7 +327,7 @@ func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
|
|||
// check the response doesn't have errors
|
||||
checkMultiAccountResponse(t, rawResp, &storeResp)
|
||||
|
||||
dir := statusBackend.StatusNode().Config().DataDir
|
||||
dir := keystoreDir
|
||||
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, storeResp.Address, password)
|
||||
if err != nil {
|
||||
t.Errorf("failed to verify password on stored derived account")
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"C"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -31,10 +31,14 @@ import (
|
|||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/signal"
|
||||
|
||||
. "github.com/status-im/status-go/t/utils" //nolint: golint
|
||||
"github.com/status-im/status-go/transactions"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
)
|
||||
import "github.com/status-im/status-go/multiaccounts/accounts"
|
||||
|
||||
const initJS = `
|
||||
var _status_catalog = {
|
||||
|
@ -44,15 +48,27 @@ const initJS = `
|
|||
var (
|
||||
zeroHash = gethcommon.Hash{}
|
||||
testChainDir string
|
||||
keystoreDir string
|
||||
nodeConfigJSON string
|
||||
)
|
||||
|
||||
func buildLoginParamsJSON(chatAddress, password string) *C.char {
|
||||
func buildAccountData(name, chatAddress string) *C.char {
|
||||
return C.CString(fmt.Sprintf(`{
|
||||
"chatAddress": "%s",
|
||||
"password": "%s",
|
||||
"mainAccount": "%s"
|
||||
}`, chatAddress, password, chatAddress))
|
||||
"name": "%s",
|
||||
"address": "%s"
|
||||
}`, name, chatAddress))
|
||||
}
|
||||
|
||||
func buildSubAccountData(chatAddress string) *C.char {
|
||||
accs := []accounts.Account{
|
||||
{
|
||||
Wallet: true,
|
||||
Chat: true,
|
||||
Address: gethcommon.HexToAddress(chatAddress),
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(accs)
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
func buildLoginParams(mainAccountAddress, chatAddress, password string) account.LoginParams {
|
||||
|
@ -63,8 +79,26 @@ func buildLoginParams(mainAccountAddress, chatAddress, password string) account.
|
|||
}
|
||||
}
|
||||
|
||||
func waitSignal(feed *event.Feed, event string, timeout time.Duration) error {
|
||||
events := make(chan signal.Envelope)
|
||||
sub := feed.Subscribe(events)
|
||||
defer sub.Unsubscribe()
|
||||
after := time.After(timeout)
|
||||
for {
|
||||
select {
|
||||
case envelope := <-events:
|
||||
if envelope.Type == event {
|
||||
return nil
|
||||
}
|
||||
case <-after:
|
||||
return fmt.Errorf("signal %v wasn't received in %v", event, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
testChainDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
|
||||
keystoreDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()], "keystore")
|
||||
|
||||
nodeConfigJSON = `{
|
||||
"NetworkId": ` + strconv.Itoa(GetNetworkID()) + `,
|
||||
|
@ -73,6 +107,7 @@ func init() {
|
|||
"HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `,
|
||||
"LogLevel": "INFO",
|
||||
"NoDiscovery": true,
|
||||
"APIModules": "web3,eth",
|
||||
"LightEthConfig": {
|
||||
"Enabled": true
|
||||
},
|
||||
|
@ -87,106 +122,123 @@ func init() {
|
|||
}`
|
||||
}
|
||||
|
||||
// nolint: deadcode
|
||||
func testExportedAPI(t *testing.T, done chan struct{}) {
|
||||
<-startTestNode(t)
|
||||
defer func() {
|
||||
done <- struct{}{}
|
||||
}()
|
||||
func createAccountAndLogin(t *testing.T, feed *event.Feed) account.Info {
|
||||
account1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
require.NoError(t, err)
|
||||
t.Logf("account created: {address: %s, key: %s}", account1.WalletAddress, account1.WalletPubKey)
|
||||
|
||||
// prepare accounts
|
||||
testKeyDir := filepath.Join(testChainDir, "keystore")
|
||||
if err := ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := ImportTestAccount(testKeyDir, GetAccount2PKFile()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// select account
|
||||
loginResponse := APIResponse{}
|
||||
rawResponse := SaveAccountAndLogin(buildAccountData("test", account1.WalletAddress), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(account1.WalletAddress))
|
||||
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
|
||||
require.Empty(t, loginResponse.Error)
|
||||
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
|
||||
return account1
|
||||
}
|
||||
|
||||
// nolint: deadcode
|
||||
func testExportedAPI(t *testing.T) {
|
||||
testDir := filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
|
||||
_ = OpenAccounts(C.CString(testDir))
|
||||
// inject test accounts
|
||||
testKeyDir := filepath.Join(testDir, "keystore")
|
||||
_ = InitKeystore(C.CString(testKeyDir))
|
||||
require.NoError(t, ImportTestAccount(testKeyDir, GetAccount1PKFile()))
|
||||
require.NoError(t, ImportTestAccount(testKeyDir, GetAccount2PKFile()))
|
||||
|
||||
// FIXME(tiabc): All of that is done because usage of cgo is not supported in tests.
|
||||
// Probably, there should be a cleaner way, for example, test cgo bindings in e2e tests
|
||||
// separately from other internal tests.
|
||||
// FIXME(@jekamas): ATTENTION! this tests depends on each other!
|
||||
// NOTE(dshulyak) tests are using same backend with same keystore. but after every test we explicitly logging out.
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(t *testing.T) bool
|
||||
fn func(t *testing.T, feed *event.Feed) bool
|
||||
}{
|
||||
{
|
||||
"stop/resume node",
|
||||
"StopResumeNode",
|
||||
testStopResumeNode,
|
||||
},
|
||||
|
||||
{
|
||||
"call RPC on in-proc handler",
|
||||
"RPCInProc",
|
||||
testCallRPC,
|
||||
},
|
||||
|
||||
{
|
||||
"call private API using RPC",
|
||||
"RPCPrivateAPI",
|
||||
testCallRPCWithPrivateAPI,
|
||||
},
|
||||
{
|
||||
"call private API using private RPC client",
|
||||
"RPCPrivateClient",
|
||||
testCallPrivateRPCWithPrivateAPI,
|
||||
},
|
||||
{
|
||||
"verify account password",
|
||||
"VerifyAccountPassword",
|
||||
testVerifyAccountPassword,
|
||||
},
|
||||
{
|
||||
"recover account",
|
||||
"RecoverAccount",
|
||||
testRecoverAccount,
|
||||
},
|
||||
{
|
||||
"account select/login",
|
||||
testAccountSelect,
|
||||
},
|
||||
{
|
||||
"login with keycard",
|
||||
"LoginKeycard",
|
||||
testLoginWithKeycard,
|
||||
},
|
||||
{
|
||||
"account logout",
|
||||
"AccountLoout",
|
||||
testAccountLogout,
|
||||
},
|
||||
{
|
||||
"send transaction",
|
||||
testSendTransaction,
|
||||
"SendTransaction",
|
||||
testSendTransactionWithLogin,
|
||||
},
|
||||
{
|
||||
"send transaction with invalid password",
|
||||
"SendTransactionInvalidPassword",
|
||||
testSendTransactionInvalidPassword,
|
||||
},
|
||||
{
|
||||
"failed single transaction",
|
||||
"SendTransactionFailed",
|
||||
testFailedTransaction,
|
||||
},
|
||||
{
|
||||
"MultiAccount - Generate/Derive/StoreDerived/Load/Reset",
|
||||
"MultiAccount/Generate/Derive/StoreDerived/Load/Reset",
|
||||
testMultiAccountGenerateDeriveStoreLoadReset,
|
||||
},
|
||||
{
|
||||
"MultiAccount - ImportMnemonic/Derive",
|
||||
"MultiAccount/ImportMnemonic/Derive",
|
||||
testMultiAccountImportMnemonicAndDerive,
|
||||
},
|
||||
{
|
||||
"MultiAccount - GenerateAndDerive",
|
||||
"MultiAccount/GenerateAndDerive",
|
||||
testMultiAccountGenerateAndDerive,
|
||||
},
|
||||
{
|
||||
"MultiAccount - Import/Store",
|
||||
"MultiAccount/Import/Store",
|
||||
testMultiAccountImportStore,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Logf("=== RUN %s", test.name)
|
||||
if ok := test.fn(t); !ok {
|
||||
t.Logf("=== FAILED %s", test.name)
|
||||
break
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
feed := &event.Feed{}
|
||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
||||
var envelope signal.Envelope
|
||||
require.NoError(t, json.Unmarshal([]byte(jsonEvent), &envelope))
|
||||
feed.Send(envelope)
|
||||
})
|
||||
defer func() {
|
||||
if snode := statusBackend.StatusNode(); snode == nil || !snode.IsRunning() {
|
||||
return
|
||||
}
|
||||
Logout()
|
||||
waitSignal(feed, signal.EventNodeStopped, 5*time.Second)
|
||||
}()
|
||||
require.True(t, tc.fn(t, feed))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testVerifyAccountPassword(t *testing.T) bool {
|
||||
func testVerifyAccountPassword(t *testing.T, feed *event.Feed) bool {
|
||||
tmpDir, err := ioutil.TempDir(os.TempDir(), "accounts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -225,137 +277,34 @@ func testVerifyAccountPassword(t *testing.T) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
//@TODO(adam): quarantined this test until it uses a different directory.
|
||||
//nolint: deadcode
|
||||
func testResetChainData(t *testing.T) bool {
|
||||
t.Skip()
|
||||
|
||||
resetChainDataResponse := APIResponse{}
|
||||
rawResponse := ResetChainData()
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &resetChainDataResponse); err != nil {
|
||||
t.Errorf("cannot decode ResetChainData response (%s): %v", C.GoString(rawResponse), err)
|
||||
return false
|
||||
}
|
||||
if resetChainDataResponse.Error != "" {
|
||||
t.Errorf("unexpected error: %s", resetChainDataResponse.Error)
|
||||
return false
|
||||
}
|
||||
|
||||
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||
testSendTransaction(t)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo
|
||||
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||
if err := statusBackend.Logout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
func testStopResumeNode(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
|
||||
account1 := createAccountAndLogin(t, feed)
|
||||
whisperService, err := statusBackend.StatusNode().WhisperService()
|
||||
if err != nil {
|
||||
t.Errorf("whisper service not running: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.True(t, whisperService.HasKeyPair(account1.ChatPubKey), "whisper should have keypair")
|
||||
|
||||
// create an account
|
||||
account1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
if err != nil {
|
||||
t.Errorf("could not create account: %v", err)
|
||||
return false
|
||||
}
|
||||
t.Logf("account created: {address: %s, key: %s}", account1.WalletAddress, account1.WalletPubKey)
|
||||
response := APIResponse{}
|
||||
rawResponse := StopNode()
|
||||
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
|
||||
require.Empty(t, response.Error)
|
||||
|
||||
// make sure that identity is not (yet injected)
|
||||
if whisperService.HasKeyPair(account1.ChatPubKey) {
|
||||
t.Error("identity already present in whisper")
|
||||
}
|
||||
require.NoError(t, waitSignal(feed, signal.EventNodeStopped, 3*time.Second))
|
||||
response = APIResponse{}
|
||||
rawResponse = StartNode(C.CString(nodeConfigJSON))
|
||||
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
|
||||
require.Empty(t, response.Error)
|
||||
|
||||
// select account
|
||||
loginResponse := APIResponse{}
|
||||
rawResponse := Login(buildLoginParamsJSON(account1.WalletAddress, TestConfig.Account1.Password))
|
||||
|
||||
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
|
||||
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
|
||||
return false
|
||||
}
|
||||
|
||||
if loginResponse.Error != "" {
|
||||
t.Errorf("could not select account: %v", err)
|
||||
return false
|
||||
}
|
||||
if !whisperService.HasKeyPair(account1.ChatPubKey) {
|
||||
t.Errorf("identity not injected into whisper: %v", err)
|
||||
}
|
||||
|
||||
// stop and resume node, then make sure that selected account is still selected
|
||||
// nolint: dupl
|
||||
stopNodeFn := func() bool {
|
||||
response := APIResponse{}
|
||||
// FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid
|
||||
// 9-sec timeout below after stopping the node.
|
||||
rawResponse = StopNode()
|
||||
|
||||
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
|
||||
t.Errorf("cannot decode StopNode response (%s): %v", C.GoString(rawResponse), err)
|
||||
return false
|
||||
}
|
||||
if response.Error != "" {
|
||||
t.Errorf("unexpected error: %s", response.Error)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// nolint: dupl
|
||||
resumeNodeFn := func() bool {
|
||||
response := APIResponse{}
|
||||
// FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid
|
||||
// 10-sec timeout below after resuming the node.
|
||||
rawResponse = StartNode(C.CString(nodeConfigJSON))
|
||||
|
||||
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
|
||||
t.Errorf("cannot decode StartNode response (%s): %v", C.GoString(rawResponse), err)
|
||||
return false
|
||||
}
|
||||
if response.Error != "" {
|
||||
t.Errorf("unexpected error: %s", response.Error)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if !stopNodeFn() {
|
||||
return false
|
||||
}
|
||||
|
||||
time.Sleep(9 * time.Second) // allow to stop
|
||||
|
||||
if !resumeNodeFn() {
|
||||
return false
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second) // allow to start (instead of using blocking version of start, of filter event)
|
||||
require.NoError(t, waitSignal(feed, signal.EventNodeReady, 5*time.Second))
|
||||
|
||||
// now, verify that we still have account logged in
|
||||
whisperService, err = statusBackend.StatusNode().WhisperService()
|
||||
if err != nil {
|
||||
t.Errorf("whisper service not running: %v", err)
|
||||
}
|
||||
if !whisperService.HasKeyPair(account1.ChatPubKey) {
|
||||
t.Errorf("identity evicted from whisper on node restart: %v", err)
|
||||
}
|
||||
|
||||
// additionally, let's complete transaction (just to make sure that node lives through pause/resume w/o issues)
|
||||
testSendTransaction(t)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, whisperService.HasKeyPair(account1.ChatPubKey))
|
||||
return true
|
||||
}
|
||||
|
||||
func testCallRPC(t *testing.T) bool {
|
||||
func testCallRPC(t *testing.T, feed *event.Feed) bool {
|
||||
createAccountAndLogin(t, feed)
|
||||
expected := `{"jsonrpc":"2.0","id":64,"result":"0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"}`
|
||||
rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f20776f726c64"],"id":64}`))
|
||||
received := C.GoString(rawResponse)
|
||||
|
@ -367,19 +316,16 @@ func testCallRPC(t *testing.T) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func testCallRPCWithPrivateAPI(t *testing.T) bool {
|
||||
func testCallRPCWithPrivateAPI(t *testing.T, feed *event.Feed) bool {
|
||||
createAccountAndLogin(t, feed)
|
||||
expected := `{"jsonrpc":"2.0","id":64,"error":{"code":-32601,"message":"The method admin_nodeInfo does not exist/is not available"}}`
|
||||
rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
|
||||
received := C.GoString(rawResponse)
|
||||
if expected != received {
|
||||
t.Errorf("unexpected response: expected: %v, got: %v", expected, received)
|
||||
return false
|
||||
}
|
||||
|
||||
require.Equal(t, expected, C.GoString(rawResponse))
|
||||
return true
|
||||
}
|
||||
|
||||
func testCallPrivateRPCWithPrivateAPI(t *testing.T) bool {
|
||||
func testCallPrivateRPCWithPrivateAPI(t *testing.T, feed *event.Feed) bool {
|
||||
createAccountAndLogin(t, feed)
|
||||
rawResponse := CallPrivateRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
|
||||
received := C.GoString(rawResponse)
|
||||
if strings.Contains(received, "error") {
|
||||
|
@ -390,9 +336,9 @@ func testCallPrivateRPCWithPrivateAPI(t *testing.T) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
|
||||
keyStore, _ := statusBackend.StatusNode().AccountKeyStore()
|
||||
|
||||
func testRecoverAccount(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
|
||||
keyStore := statusBackend.AccountManager().GetKeystore()
|
||||
require.NotNil(t, keyStore)
|
||||
// create an account
|
||||
accountInfo, mnemonic, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
if err != nil {
|
||||
|
@ -503,21 +449,18 @@ func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
|
|||
t.Error("recover chat account details failed to pull the correct details (for non-cached account)")
|
||||
}
|
||||
|
||||
loginResponse := APIResponse{}
|
||||
rawResponse = SaveAccountAndLogin(buildAccountData("test", walletAddressCheck), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(walletAddressCheck))
|
||||
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
|
||||
require.Empty(t, loginResponse.Error)
|
||||
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
|
||||
|
||||
// time to login with recovered data
|
||||
whisperService, err := statusBackend.StatusNode().WhisperService()
|
||||
if err != nil {
|
||||
t.Errorf("whisper service not running: %v", err)
|
||||
}
|
||||
|
||||
// make sure that identity is not (yet injected)
|
||||
if whisperService.HasKeyPair(chatPubKeyCheck) {
|
||||
t.Error("identity already present in whisper")
|
||||
}
|
||||
err = statusBackend.SelectAccount(buildLoginParams(walletAddressCheck, chatAddressCheck, TestConfig.Account1.Password))
|
||||
if err != nil {
|
||||
t.Errorf("Test failed: could not select account: %v", err)
|
||||
return false
|
||||
}
|
||||
if !whisperService.HasKeyPair(chatPubKeyCheck) {
|
||||
t.Errorf("identity not injected into whisper: %v", err)
|
||||
}
|
||||
|
@ -525,91 +468,8 @@ func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
|
|||
return true
|
||||
}
|
||||
|
||||
func testAccountSelect(t *testing.T) bool { //nolint: gocyclo
|
||||
// test to see if the account was injected in whisper
|
||||
whisperService, err := statusBackend.StatusNode().WhisperService()
|
||||
if err != nil {
|
||||
t.Errorf("whisper service not running: %v", err)
|
||||
}
|
||||
|
||||
// create an account
|
||||
accountInfo1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
if err != nil {
|
||||
t.Errorf("could not create account: %v", err)
|
||||
return false
|
||||
}
|
||||
t.Logf("Account created: {address: %s, key: %s}", accountInfo1.WalletAddress, accountInfo1.WalletPubKey)
|
||||
|
||||
accountInfo2, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
if err != nil {
|
||||
t.Error("Test failed: could not create account")
|
||||
return false
|
||||
}
|
||||
t.Logf("Account created: {address: %s, key: %s}", accountInfo2.WalletAddress, accountInfo2.WalletPubKey)
|
||||
|
||||
// make sure that identity is not (yet injected)
|
||||
if whisperService.HasKeyPair(accountInfo1.ChatPubKey) {
|
||||
t.Error("identity already present in whisper")
|
||||
}
|
||||
|
||||
// try selecting with wrong password
|
||||
loginResponse := APIResponse{}
|
||||
rawResponse := Login(buildLoginParamsJSON(accountInfo1.WalletAddress, "wrongPassword"))
|
||||
|
||||
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
|
||||
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
|
||||
return false
|
||||
}
|
||||
|
||||
if loginResponse.Error == "" {
|
||||
t.Error("select account is expected to throw error: wrong password used")
|
||||
return false
|
||||
}
|
||||
|
||||
loginResponse = APIResponse{}
|
||||
rawResponse = Login(buildLoginParamsJSON(accountInfo1.WalletAddress, TestConfig.Account1.Password))
|
||||
|
||||
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
|
||||
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
|
||||
return false
|
||||
}
|
||||
|
||||
if loginResponse.Error != "" {
|
||||
t.Errorf("Test failed: could not select account: %v", err)
|
||||
return false
|
||||
}
|
||||
if !whisperService.HasKeyPair(accountInfo1.ChatPubKey) {
|
||||
t.Errorf("identity not injected into whisper: %v", err)
|
||||
}
|
||||
|
||||
// select another account, make sure that previous account is wiped out from Whisper cache
|
||||
if whisperService.HasKeyPair(accountInfo2.ChatPubKey) {
|
||||
t.Error("identity already present in whisper")
|
||||
}
|
||||
|
||||
loginResponse = APIResponse{}
|
||||
rawResponse = Login(buildLoginParamsJSON(accountInfo2.WalletAddress, TestConfig.Account1.Password))
|
||||
|
||||
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
|
||||
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
|
||||
return false
|
||||
}
|
||||
|
||||
if loginResponse.Error != "" {
|
||||
t.Errorf("Test failed: could not select account: %v", loginResponse.Error)
|
||||
return false
|
||||
}
|
||||
if !whisperService.HasKeyPair(accountInfo2.ChatPubKey) {
|
||||
t.Errorf("identity not injected into whisper: %v", err)
|
||||
}
|
||||
if whisperService.HasKeyPair(accountInfo1.ChatPubKey) {
|
||||
t.Error("identity should be removed, but it is still present in whisper")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func testLoginWithKeycard(t *testing.T) bool { //nolint: gocyclo
|
||||
func testLoginWithKeycard(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
|
||||
createAccountAndLogin(t, feed)
|
||||
chatPrivKey, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Errorf("error generating chat key")
|
||||
|
@ -656,32 +516,14 @@ func testLoginWithKeycard(t *testing.T) bool { //nolint: gocyclo
|
|||
return true
|
||||
}
|
||||
|
||||
func testAccountLogout(t *testing.T) bool {
|
||||
func testAccountLogout(t *testing.T, feed *event.Feed) bool {
|
||||
accountInfo := createAccountAndLogin(t, feed)
|
||||
whisperService, err := statusBackend.StatusNode().WhisperService()
|
||||
if err != nil {
|
||||
t.Errorf("whisper service not running: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// create an account
|
||||
accountInfo, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
if err != nil {
|
||||
t.Errorf("could not create account: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// make sure that identity doesn't exist (yet) in Whisper
|
||||
if whisperService.HasKeyPair(accountInfo.ChatPubKey) {
|
||||
t.Error("identity already present in whisper")
|
||||
return false
|
||||
}
|
||||
|
||||
// select/login
|
||||
err = statusBackend.SelectAccount(buildLoginParams(accountInfo.WalletAddress, accountInfo.ChatAddress, TestConfig.Account1.Password))
|
||||
if err != nil {
|
||||
t.Errorf("Test failed: could not select account: %v", err)
|
||||
return false
|
||||
}
|
||||
if !whisperService.HasKeyPair(accountInfo.ChatPubKey) {
|
||||
t.Error("identity not injected into whisper")
|
||||
return false
|
||||
|
@ -714,15 +556,14 @@ type jsonrpcAnyResponse struct {
|
|||
jsonrpcErrorResponse
|
||||
}
|
||||
|
||||
func testSendTransaction(t *testing.T) bool {
|
||||
func testSendTransactionWithLogin(t *testing.T, feed *event.Feed) bool {
|
||||
loginResponse := APIResponse{}
|
||||
rawResponse := SaveAccountAndLogin(buildAccountData("test", TestConfig.Account1.WalletAddress), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(TestConfig.Account1.WalletAddress))
|
||||
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
|
||||
require.Empty(t, loginResponse.Error)
|
||||
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
|
||||
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||
|
||||
// log into account from which transactions will be sent
|
||||
if err := statusBackend.SelectAccount(buildLoginParams(TestConfig.Account1.WalletAddress, TestConfig.Account1.ChatAddress, TestConfig.Account1.Password)); err != nil {
|
||||
t.Errorf("cannot select account: %v. Error %q", TestConfig.Account1.WalletAddress, err)
|
||||
return false
|
||||
}
|
||||
|
||||
args, err := json.Marshal(transactions.SendTxArgs{
|
||||
From: account.FromAddress(TestConfig.Account1.WalletAddress),
|
||||
To: account.ToAddress(TestConfig.Account2.WalletAddress),
|
||||
|
@ -752,7 +593,8 @@ func testSendTransaction(t *testing.T) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func testSendTransactionInvalidPassword(t *testing.T) bool {
|
||||
func testSendTransactionInvalidPassword(t *testing.T, feed *event.Feed) bool {
|
||||
createAccountAndLogin(t, feed)
|
||||
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||
|
||||
// log into account from which transactions will be sent
|
||||
|
@ -789,7 +631,8 @@ func testSendTransactionInvalidPassword(t *testing.T) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func testFailedTransaction(t *testing.T) bool {
|
||||
func testFailedTransaction(t *testing.T, feed *event.Feed) bool {
|
||||
createAccountAndLogin(t, feed)
|
||||
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||
|
||||
// log into wrong account in order to get selectedAccount error
|
||||
|
@ -829,70 +672,6 @@ func testFailedTransaction(t *testing.T) bool {
|
|||
|
||||
}
|
||||
|
||||
func startTestNode(t *testing.T) <-chan struct{} {
|
||||
testDir := filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
|
||||
|
||||
syncRequired := false
|
||||
if _, err := os.Stat(testDir); os.IsNotExist(err) {
|
||||
syncRequired = true
|
||||
}
|
||||
|
||||
// inject test accounts
|
||||
testKeyDir := filepath.Join(testDir, "keystore")
|
||||
if err := ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := ImportTestAccount(testKeyDir, GetAccount2PKFile()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
waitForNodeStart := make(chan struct{}, 1)
|
||||
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
||||
t.Log(jsonEvent)
|
||||
var envelope signal.Envelope
|
||||
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
|
||||
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
|
||||
return
|
||||
}
|
||||
if envelope.Type == signal.EventNodeCrashed {
|
||||
signal.TriggerDefaultNodeNotificationHandler(jsonEvent)
|
||||
return
|
||||
}
|
||||
|
||||
if envelope.Type == signal.EventSignRequestAdded {
|
||||
}
|
||||
if envelope.Type == signal.EventNodeStarted {
|
||||
t.Log("Node started, but we wait till it be ready")
|
||||
}
|
||||
if envelope.Type == signal.EventNodeReady {
|
||||
// sync
|
||||
if syncRequired {
|
||||
t.Logf("Sync is required")
|
||||
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
|
||||
} else {
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
// now we can proceed with tests
|
||||
waitForNodeStart <- struct{}{}
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
response := StartNode(C.CString(nodeConfigJSON))
|
||||
responseErr := APIResponse{}
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(response)), &responseErr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if responseErr.Error != "" {
|
||||
panic("cannot start node: " + responseErr.Error)
|
||||
}
|
||||
}()
|
||||
|
||||
return waitForNodeStart
|
||||
}
|
||||
|
||||
//nolint: deadcode
|
||||
func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, APIDetailedResponse)) {
|
||||
result := ValidateNodeConfig(C.CString(config))
|
||||
|
|
104
mobile/status.go
104
mobile/status.go
|
@ -10,10 +10,10 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/api"
|
||||
"github.com/status-im/status-go/exportlogs"
|
||||
"github.com/status-im/status-go/logutils"
|
||||
"github.com/status-im/status-go/multiaccounts"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/profiling"
|
||||
"github.com/status-im/status-go/services/personal"
|
||||
|
@ -28,6 +28,24 @@ var statusBackend = api.NewStatusBackend()
|
|||
// All general log messages in this package should be routed through this logger.
|
||||
var logger = log.New("package", "status-go/mobile")
|
||||
|
||||
// OpenAccounts opens database and returns accounts list.
|
||||
func OpenAccounts(datadir string) string {
|
||||
statusBackend.UpdateRootDataDir(datadir)
|
||||
err := statusBackend.OpenAccounts()
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
accs, err := statusBackend.GetAccounts()
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
data, err := json.Marshal(accs)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// GenerateConfig for status node.
|
||||
func GenerateConfig(datadir string, networkID int) string {
|
||||
config, err := params.NewNodeConfig(datadir, uint64(networkID))
|
||||
|
@ -43,28 +61,6 @@ func GenerateConfig(datadir string, networkID int) string {
|
|||
return string(outBytes)
|
||||
}
|
||||
|
||||
// StartNode starts the Ethereum Status node.
|
||||
func StartNode(configJSON string) string {
|
||||
config, err := params.NewConfigFromJSON(configJSON)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
if err := logutils.OverrideRootLogWithConfig(config, false); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
api.RunAsync(func() error { return statusBackend.StartNode(config) })
|
||||
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
// StopNode stops the Ethereum Status node.
|
||||
func StopNode() string {
|
||||
api.RunAsync(statusBackend.StopNode)
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature.
|
||||
func ExtractGroupMembershipSignatures(signaturePairsStr string) string {
|
||||
var signaturePairs [][2]string
|
||||
|
@ -209,7 +205,6 @@ func CallPrivateRPC(inputJSON string) string {
|
|||
// just modified to handle the function arg passing.
|
||||
func CreateAccount(password string) string {
|
||||
info, mnemonic, err := statusBackend.AccountManager().CreateAccount(password)
|
||||
|
||||
errString := ""
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
@ -326,13 +321,58 @@ func VerifyAccountPassword(keyStoreDir, address, password string) string {
|
|||
// Login loads a key file (for a given address), tries to decrypt it using the password,
|
||||
// to verify ownership if verified, purges all the previous identities from Whisper,
|
||||
// and injects verified key as shh identity.
|
||||
func Login(loginParamsJSON string) string {
|
||||
params, err := account.ParseLoginParams(loginParamsJSON)
|
||||
func Login(accountData, password string) string {
|
||||
var account multiaccounts.Account
|
||||
err := json.Unmarshal([]byte(accountData), &account)
|
||||
if err != nil {
|
||||
return prepareJSONResponseWithCode(nil, err, codeFailedParseParams)
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
api.RunAsync(func() error {
|
||||
log.Debug("start a node with account", "address", account.Address)
|
||||
err := statusBackend.StartNodeWithAccount(account, password)
|
||||
if err != nil {
|
||||
log.Error("failed to start a node", "address", account.Address, "error", err)
|
||||
return err
|
||||
}
|
||||
log.Debug("started a node with", "address", account.Address)
|
||||
return nil
|
||||
})
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
err = statusBackend.SelectAccount(params)
|
||||
// SaveAccountAndLogin saves account in status-go database..
|
||||
func SaveAccountAndLogin(accountData, password, configJSON, subaccountData string) string {
|
||||
var account multiaccounts.Account
|
||||
err := json.Unmarshal([]byte(accountData), &account)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
var conf params.NodeConfig
|
||||
err = json.Unmarshal([]byte(configJSON), &conf)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
var subaccs []accounts.Account
|
||||
err = json.Unmarshal([]byte(subaccountData), &subaccs)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
api.RunAsync(func() error {
|
||||
log.Debug("starting a node, and saving account with configuration", "address", account.Address)
|
||||
err := statusBackend.StartNodeWithAccountAndConfig(account, password, &conf, subaccs)
|
||||
if err != nil {
|
||||
log.Error("failed to start node and save account", "address", account.Address, "error", err)
|
||||
return err
|
||||
}
|
||||
log.Debug("started a node, and saved account", "address", account.Address)
|
||||
return nil
|
||||
})
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
// InitKeystore initialize keystore before doing any operations with keys.
|
||||
func InitKeystore(keydir string) string {
|
||||
err := statusBackend.AccountManager().InitKeystore(keydir)
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
|
@ -346,7 +386,11 @@ func LoginWithKeycard(chatKeyData, encryptionKeyData string) string {
|
|||
// Logout is equivalent to clearing whisper identities.
|
||||
func Logout() string {
|
||||
err := statusBackend.Logout()
|
||||
return makeJSONResponse(err)
|
||||
if err != nil {
|
||||
makeJSONResponse(err)
|
||||
}
|
||||
api.RunAsync(statusBackend.StopNode)
|
||||
return makeJSONResponse(nil)
|
||||
}
|
||||
|
||||
// SignMessage unmarshals rpc params {data, address, password} and
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package accounts
|
||||
|
||||
const (
|
||||
// NodeConfigTag tag for a node configuration.
|
||||
NodeConfigTag = "node-config"
|
||||
)
|
|
@ -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{
|
||||
}},
|
||||
}}
|
|
@ -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)
|
||||
},
|
||||
))
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE settings;
|
||||
DROP TABLE accounts;
|
|
@ -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);
|
|
@ -0,0 +1,3 @@
|
|||
package sql
|
||||
|
||||
//go:generate go-bindata -pkg migrations -o ../bindata.go ./
|
|
@ -0,0 +1,77 @@
|
|||
package multiaccounts
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/multiaccounts/migrations"
|
||||
"github.com/status-im/status-go/sqlite"
|
||||
)
|
||||
|
||||
// Account stores public information about account.
|
||||
type Account struct {
|
||||
Name string `json:"name"`
|
||||
Address common.Address `json:"address"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
PhotoPath string `json:"photo-path"`
|
||||
}
|
||||
|
||||
// InitializeDB creates db file at a given path and applies migrations.
|
||||
func InitializeDB(path string) (*Database, error) {
|
||||
db, err := sqlite.OpenUnecryptedDB(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = migrations.Migrate(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Database{db: db}, nil
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (db *Database) Close() error {
|
||||
return db.db.Close()
|
||||
}
|
||||
|
||||
func (db *Database) GetAccounts() ([]Account, error) {
|
||||
rows, err := db.db.Query("SELECT address, name, loginTimestamp, photoPath from accounts ORDER BY loginTimestamp DESC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rst := []Account{}
|
||||
inthelper := sql.NullInt64{}
|
||||
for rows.Next() {
|
||||
acc := Account{}
|
||||
err = rows.Scan(&acc.Address, &acc.Name, &inthelper, &acc.PhotoPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc.Timestamp = inthelper.Int64
|
||||
rst = append(rst, acc)
|
||||
}
|
||||
return rst, nil
|
||||
}
|
||||
|
||||
func (db *Database) SaveAccount(account Account) error {
|
||||
_, err := db.db.Exec("INSERT OR REPLACE INTO accounts (address, name, photoPath) VALUES (?, ?, ?)", account.Address, account.Name, account.PhotoPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) UpdateAccount(account Account) error {
|
||||
_, err := db.db.Exec("UPDATE accounts SET name = ?, photoPath = ? WHERE address = ?", account.Name, account.PhotoPath, account.Address)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) UpdateAccountTimestamp(address common.Address, loginTimestamp int64) error {
|
||||
_, err := db.db.Exec("UPDATE accounts SET loginTimestamp = ? WHERE address = ?", loginTimestamp, address)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) DeleteAccount(address common.Address) error {
|
||||
_, err := db.db.Exec("DELETE FROM accounts WHERE address = ?", address)
|
||||
return err
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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{
|
||||
}},
|
||||
}}
|
|
@ -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)
|
||||
},
|
||||
))
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE accounts;
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
package sql
|
||||
|
||||
//go:generate go-bindata -pkg migrations -o ../bindata.go ./
|
24
node/node.go
24
node/node.go
|
@ -9,6 +9,7 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
@ -57,7 +58,7 @@ var (
|
|||
var logger = log.New("package", "status-go/node")
|
||||
|
||||
// MakeNode creates a geth node entity
|
||||
func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
|
||||
func MakeNode(config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB) (*node.Node, error) {
|
||||
// If DataDir is empty, it means we want to create an ephemeral node
|
||||
// keeping data only in memory.
|
||||
if config.DataDir != "" {
|
||||
|
@ -82,17 +83,17 @@ func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
|
|||
return nil, fmt.Errorf(ErrNodeMakeFailureFormat, err.Error())
|
||||
}
|
||||
|
||||
err = activateServices(stack, config, db)
|
||||
err = activateServices(stack, config, accs, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.DB) error {
|
||||
func activateServices(stack *node.Node, config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB) error {
|
||||
// start Ethereum service if we are not expected to use an upstream server
|
||||
if !config.UpstreamConfig.Enabled {
|
||||
if err := activateLightEthService(stack, config); err != nil {
|
||||
if err := activateLightEthService(stack, accs, config); err != nil {
|
||||
return fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err)
|
||||
}
|
||||
} else {
|
||||
|
@ -107,7 +108,7 @@ func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.D
|
|||
// Usually, they are provided by an ETH or a LES service, but when using
|
||||
// upstream, we don't start any of these, so we need to start our own
|
||||
// implementation.
|
||||
if err := activatePersonalService(stack, config); err != nil {
|
||||
if err := activatePersonalService(stack, accs, config); err != nil {
|
||||
return fmt.Errorf("%v: %v", ErrPersonalServiceRegistrationFailure, err)
|
||||
}
|
||||
}
|
||||
|
@ -248,7 +249,7 @@ func defaultStatusChainGenesisBlock() (*core.Genesis, error) {
|
|||
}
|
||||
|
||||
// activateLightEthService configures and registers the eth.Ethereum service with a given node.
|
||||
func activateLightEthService(stack *node.Node, config *params.NodeConfig) error {
|
||||
func activateLightEthService(stack *node.Node, accs *accounts.Manager, config *params.NodeConfig) error {
|
||||
if !config.LightEthConfig.Enabled {
|
||||
logger.Info("LES protocol is disabled")
|
||||
return nil
|
||||
|
@ -269,13 +270,18 @@ func activateLightEthService(stack *node.Node, config *params.NodeConfig) error
|
|||
MinTrustedFraction: config.LightEthConfig.MinTrustedFraction,
|
||||
}
|
||||
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
return les.New(ctx, ðConf)
|
||||
// NOTE(dshulyak) here we set our instance of the accounts manager.
|
||||
// without sharing same instance selected account won't be visible for personal_* methods.
|
||||
nctx := &node.ServiceContext{}
|
||||
*nctx = *ctx
|
||||
nctx.AccountManager = accs
|
||||
return les.New(nctx, ðConf)
|
||||
})
|
||||
}
|
||||
|
||||
func activatePersonalService(stack *node.Node, config *params.NodeConfig) error {
|
||||
func activatePersonalService(stack *node.Node, accs *accounts.Manager, config *params.NodeConfig) error {
|
||||
return stack.Register(func(*node.ServiceContext) (node.Service, error) {
|
||||
svc := personal.New(stack.AccountManager())
|
||||
svc := personal.New(accs)
|
||||
return svc, nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package node
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
whisper "github.com/status-im/whisper/whisperv6"
|
||||
|
||||
"github.com/status-im/status-go/params"
|
||||
|
@ -17,7 +18,7 @@ func TestWhisperLightModeEnabledSetsEmptyBloomFilter(t *testing.T) {
|
|||
},
|
||||
}
|
||||
node := New()
|
||||
require.NoError(t, node.Start(&config))
|
||||
require.NoError(t, node.Start(&config, &accounts.Manager{}))
|
||||
defer func() {
|
||||
require.NoError(t, node.Stop())
|
||||
}()
|
||||
|
@ -39,7 +40,7 @@ func TestWhisperLightModeEnabledSetsNilBloomFilter(t *testing.T) {
|
|||
},
|
||||
}
|
||||
node := New()
|
||||
require.NoError(t, node.Start(&config))
|
||||
require.NoError(t, node.Start(&config, &accounts.Manager{}))
|
||||
defer func() {
|
||||
require.NoError(t, node.Stop())
|
||||
}()
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/status-im/status-go/params"
|
||||
|
@ -23,7 +24,7 @@ func TestMakeNodeDefaultConfig(t *testing.T) {
|
|||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = MakeNode(config, db)
|
||||
_, err = MakeNode(config, &accounts.Manager{}, db)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -40,7 +41,7 @@ func TestMakeNodeWellFormedBootnodes(t *testing.T) {
|
|||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = MakeNode(config, db)
|
||||
_, err = MakeNode(config, &accounts.Manager{}, db)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -58,7 +59,7 @@ func TestMakeNodeMalformedBootnodes(t *testing.T) {
|
|||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = MakeNode(config, db)
|
||||
_, err = MakeNode(config, &accounts.Manager{}, db)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
|
@ -105,17 +104,19 @@ func (n *StatusNode) Server() *p2p.Server {
|
|||
|
||||
// Start starts current StatusNode, failing if it's already started.
|
||||
// It accepts a list of services that should be added to the node.
|
||||
func (n *StatusNode) Start(config *params.NodeConfig, services ...node.ServiceConstructor) error {
|
||||
func (n *StatusNode) Start(config *params.NodeConfig, accs *accounts.Manager, services ...node.ServiceConstructor) error {
|
||||
return n.StartWithOptions(config, StartOptions{
|
||||
Services: services,
|
||||
StartDiscovery: true,
|
||||
Services: services,
|
||||
StartDiscovery: true,
|
||||
AccountsManager: accs,
|
||||
})
|
||||
}
|
||||
|
||||
// StartOptions allows to control some parameters of Start() method.
|
||||
type StartOptions struct {
|
||||
Services []node.ServiceConstructor
|
||||
StartDiscovery bool
|
||||
Services []node.ServiceConstructor
|
||||
StartDiscovery bool
|
||||
AccountsManager *accounts.Manager
|
||||
}
|
||||
|
||||
// StartWithOptions starts current StatusNode, failing if it's already started.
|
||||
|
@ -133,12 +134,12 @@ func (n *StatusNode) StartWithOptions(config *params.NodeConfig, options StartOp
|
|||
|
||||
db, err := db.Create(config.DataDir, params.StatusDatabase)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to create database at %s: %v", config.DataDir, err)
|
||||
}
|
||||
|
||||
n.db = db
|
||||
|
||||
err = n.startWithDB(config, db, options.Services)
|
||||
err = n.startWithDB(config, options.AccountsManager, db, options.Services)
|
||||
|
||||
// continue only if there was no error when starting node with a db
|
||||
if err == nil && options.StartDiscovery && n.discoveryEnabled() {
|
||||
|
@ -156,8 +157,8 @@ func (n *StatusNode) StartWithOptions(config *params.NodeConfig, options StartOp
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *StatusNode) startWithDB(config *params.NodeConfig, db *leveldb.DB, services []node.ServiceConstructor) error {
|
||||
if err := n.createNode(config, db); err != nil {
|
||||
func (n *StatusNode) startWithDB(config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB, services []node.ServiceConstructor) error {
|
||||
if err := n.createNode(config, accs, db); err != nil {
|
||||
return err
|
||||
}
|
||||
n.config = config
|
||||
|
@ -173,8 +174,8 @@ func (n *StatusNode) startWithDB(config *params.NodeConfig, db *leveldb.DB, serv
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *StatusNode) createNode(config *params.NodeConfig, db *leveldb.DB) (err error) {
|
||||
n.gethNode, err = MakeNode(config, db)
|
||||
func (n *StatusNode) createNode(config *params.NodeConfig, accs *accounts.Manager, db *leveldb.DB) (err error) {
|
||||
n.gethNode, err = MakeNode(config, accs, db)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -638,29 +639,6 @@ func (n *StatusNode) AccountManager() (*accounts.Manager, error) {
|
|||
return n.gethNode.AccountManager(), nil
|
||||
}
|
||||
|
||||
// AccountKeyStore exposes reference to accounts key store
|
||||
func (n *StatusNode) AccountKeyStore() (*keystore.KeyStore, error) {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
if n.gethNode == nil {
|
||||
return nil, ErrNoGethNode
|
||||
}
|
||||
|
||||
accountManager := n.gethNode.AccountManager()
|
||||
backends := accountManager.Backends(keystore.KeyStoreType)
|
||||
if len(backends) == 0 {
|
||||
return nil, ErrAccountKeyStoreMissing
|
||||
}
|
||||
|
||||
keyStore, ok := backends[0].(*keystore.KeyStore)
|
||||
if !ok {
|
||||
return nil, ErrAccountKeyStoreMissing
|
||||
}
|
||||
|
||||
return keyStore, nil
|
||||
}
|
||||
|
||||
// RPCClient exposes reference to RPC client connected to the running node.
|
||||
func (n *StatusNode) RPCClient() *rpc.Client {
|
||||
n.mu.RLock()
|
||||
|
|
|
@ -58,7 +58,7 @@ func createAndStartStatusNode(config *params.NodeConfig) (*node.StatusNode, erro
|
|||
},
|
||||
}
|
||||
statusNode := node.New()
|
||||
return statusNode, statusNode.Start(config, services...)
|
||||
return statusNode, statusNode.Start(config, nil, services...)
|
||||
}
|
||||
|
||||
func TestNodeRPCClientCallOnlyPublicAPIs(t *testing.T) {
|
||||
|
|
|
@ -36,13 +36,8 @@ func TestStatusNodeStart(t *testing.T) {
|
|||
require.Nil(t, n.Config())
|
||||
require.Nil(t, n.RPCClient())
|
||||
require.Equal(t, 0, n.PeerCount())
|
||||
_, err = n.AccountManager()
|
||||
require.EqualError(t, err, ErrNoGethNode.Error())
|
||||
_, err = n.AccountKeyStore()
|
||||
require.EqualError(t, err, ErrNoGethNode.Error())
|
||||
|
||||
// start node
|
||||
require.NoError(t, n.Start(config))
|
||||
require.NoError(t, n.Start(config, nil))
|
||||
|
||||
// checks after node is started
|
||||
require.True(t, n.IsRunning())
|
||||
|
@ -53,11 +48,8 @@ func TestStatusNodeStart(t *testing.T) {
|
|||
accountManager, err := n.AccountManager()
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, accountManager)
|
||||
keyStore, err := n.AccountKeyStore()
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, keyStore)
|
||||
// try to start already started node
|
||||
require.EqualError(t, n.Start(config), ErrNodeRunning.Error())
|
||||
require.EqualError(t, n.Start(config, nil), ErrNodeRunning.Error())
|
||||
|
||||
// stop node
|
||||
require.NoError(t, n.Stop())
|
||||
|
@ -68,10 +60,6 @@ func TestStatusNodeStart(t *testing.T) {
|
|||
require.Nil(t, n.GethNode())
|
||||
require.Nil(t, n.RPCClient())
|
||||
require.Equal(t, 0, n.PeerCount())
|
||||
_, err = n.AccountManager()
|
||||
require.EqualError(t, err, ErrNoGethNode.Error())
|
||||
_, err = n.AccountKeyStore()
|
||||
require.EqualError(t, err, ErrNoGethNode.Error())
|
||||
}
|
||||
|
||||
func TestStatusNodeWithDataDir(t *testing.T) {
|
||||
|
@ -94,7 +82,7 @@ func TestStatusNodeWithDataDir(t *testing.T) {
|
|||
}
|
||||
n := New()
|
||||
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
require.NoError(t, n.Stop())
|
||||
}
|
||||
|
||||
|
@ -140,7 +128,7 @@ func TestStatusNodeServiceGetters(t *testing.T) {
|
|||
require.Nil(t, instance)
|
||||
|
||||
// start node
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
|
||||
// checks after node is started
|
||||
instance, err = service.getter()
|
||||
|
@ -184,7 +172,7 @@ func TestStatusNodeAddPeer(t *testing.T) {
|
|||
config := params.NodeConfig{
|
||||
MaxPeers: math.MaxInt32,
|
||||
}
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
defer func() { require.NoError(t, n.Stop()) }()
|
||||
|
||||
errCh := helpers.WaitForPeerAsync(n.Server(), peerURL, p2p.PeerEventTypeAdd, time.Second*5)
|
||||
|
@ -226,7 +214,7 @@ func TestStatusNodeReconnectStaticPeers(t *testing.T) {
|
|||
StaticNodes: []string{peerURL},
|
||||
},
|
||||
}
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
defer func() { require.NoError(t, n.Stop()) }()
|
||||
|
||||
// checks after node is started
|
||||
|
@ -286,7 +274,7 @@ func TestStatusNodeRendezvousDiscovery(t *testing.T) {
|
|||
AdvertiseAddr: "127.0.0.1",
|
||||
}
|
||||
n := New()
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
require.NotNil(t, n.discovery)
|
||||
require.True(t, n.discovery.Running())
|
||||
require.IsType(t, &discovery.Rendezvous{}, n.discovery)
|
||||
|
@ -320,7 +308,7 @@ func TestStatusNodeDiscoverNode(t *testing.T) {
|
|||
ListenAddr: "127.0.0.1:0",
|
||||
}
|
||||
n := New()
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
node, err := n.discoverNode()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, net.ParseIP("127.0.0.1").To4(), node.IP())
|
||||
|
@ -331,7 +319,7 @@ func TestStatusNodeDiscoverNode(t *testing.T) {
|
|||
ListenAddr: "127.0.0.1:0",
|
||||
}
|
||||
n = New()
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
node, err = n.discoverNode()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, net.ParseIP("127.0.0.2").To4(), node.IP())
|
||||
|
@ -357,7 +345,7 @@ func TestChaosModeCheckRPCClientsUpstreamURL(t *testing.T) {
|
|||
},
|
||||
}
|
||||
n := New()
|
||||
require.NoError(t, n.Start(&config))
|
||||
require.NoError(t, n.Start(&config, nil))
|
||||
defer func() { require.NoError(t, n.Stop()) }()
|
||||
require.NotNil(t, n.RPCClient())
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -16,6 +16,9 @@ const (
|
|||
|
||||
// EventChainDataRemoved is triggered when node's chain data is removed
|
||||
EventChainDataRemoved = "chaindata.removed"
|
||||
|
||||
// EventLoggedIn is once node was injected with user account and ready to be used.
|
||||
EventLoggedIn = "node.login"
|
||||
)
|
||||
|
||||
// NodeCrashEvent is special kind of error, used to report node crashes
|
||||
|
@ -53,3 +56,7 @@ func SendNodeStopped() {
|
|||
func SendChainDataRemoved() {
|
||||
send(EventChainDataRemoved, nil)
|
||||
}
|
||||
|
||||
func SendLoggedIn() {
|
||||
send(EventLoggedIn, nil)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -8,11 +8,15 @@ import (
|
|||
_ "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation
|
||||
)
|
||||
|
||||
// The reduced number of kdf iterations (for performance reasons) which is
|
||||
// currently used for derivation of the database key
|
||||
// https://github.com/status-im/status-go/pull/1343
|
||||
// https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA
|
||||
const kdfIterationsNumber = 3200
|
||||
const (
|
||||
// The reduced number of kdf iterations (for performance reasons) which is
|
||||
// currently used for derivation of the database key
|
||||
// https://github.com/status-im/status-go/pull/1343
|
||||
// https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA
|
||||
kdfIterationsNumber = 3200
|
||||
// WALMode for sqlite.
|
||||
WALMode = "wal"
|
||||
)
|
||||
|
||||
func openDB(path, key string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", path)
|
||||
|
@ -43,7 +47,7 @@ func openDB(path, key string) (*sql.DB, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mode != "wal" {
|
||||
if mode != WALMode {
|
||||
return nil, fmt.Errorf("unable to set journal_mode to WAL. actual mode %s", mode)
|
||||
}
|
||||
|
||||
|
@ -54,3 +58,31 @@ func openDB(path, key string) (*sql.DB, error) {
|
|||
func OpenDB(path, key string) (*sql.DB, error) {
|
||||
return openDB(path, key)
|
||||
}
|
||||
|
||||
// OpenUnecryptedDB opens database with setting PRAGMA key.
|
||||
func OpenUnecryptedDB(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Disable concurrent access as not supported by the driver
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
if _, err = db.Exec("PRAGMA foreign_keys=ON"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// readers do not block writers and faster i/o operations
|
||||
// https://www.sqlite.org/draft/wal.html
|
||||
// must be set after db is encrypted
|
||||
var mode string
|
||||
err = db.QueryRow("PRAGMA journal_mode=WAL").Scan(&mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mode != WALMode {
|
||||
return nil, fmt.Errorf("unable to set journal_mode to WAL. actual mode %s", mode)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ func (s *DevNodeSuite) SetupTest() {
|
|||
config.WalletConfig.Enabled = true
|
||||
config.UpstreamConfig.URL = s.miner.IPCEndpoint()
|
||||
s.backend = api.NewStatusBackend()
|
||||
s.Require().NoError(s.backend.AccountManager().InitKeystore(config.KeyStoreDir))
|
||||
s.Require().NoError(s.backend.StartNode(config))
|
||||
s.Remote, err = s.miner.Attach()
|
||||
s.Require().NoError(err)
|
||||
|
|
|
@ -67,8 +67,7 @@ func (s *AccountsTestSuite) TestImportSingleExtendedKey() {
|
|||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
|
||||
s.NoError(err)
|
||||
keyStore := s.Backend.AccountManager().GetKeystore()
|
||||
s.NotNil(keyStore)
|
||||
|
||||
// create a master extended key
|
||||
|
@ -95,8 +94,7 @@ func (s *AccountsTestSuite) TestImportAccount() {
|
|||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
|
||||
s.NoError(err)
|
||||
keyStore := s.Backend.AccountManager().GetKeystore()
|
||||
s.NotNil(keyStore)
|
||||
|
||||
// create a private key
|
||||
|
@ -119,8 +117,8 @@ func (s *AccountsTestSuite) TestRecoverAccount() {
|
|||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
|
||||
s.NoError(err)
|
||||
keyStore := s.Backend.AccountManager().GetKeystore()
|
||||
s.NotNil(keyStore)
|
||||
|
||||
// create an acc
|
||||
accountInfo, mnemonic, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
|
|
|
@ -90,6 +90,7 @@ func (s *APITestSuite) TestRaceConditions() {
|
|||
if rnd.Intn(100) > 75 { // introduce random delays
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
s.NoError(s.backend.AccountManager().InitKeystore(randConfig.KeyStoreDir))
|
||||
go randFunc(randConfig)
|
||||
}
|
||||
|
||||
|
@ -124,6 +125,7 @@ func (s *APITestSuite) TestEventsNodeStartStop() {
|
|||
|
||||
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
|
||||
s.NoError(err)
|
||||
s.NoError(s.backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
|
||||
s.Require().NoError(s.backend.StartNode(nodeConfig))
|
||||
s.NoError(s.backend.StopNode())
|
||||
s.verifyEnvelopes(envelopes, signal.EventNodeStarted, signal.EventNodeReady, signal.EventNodeStopped)
|
||||
|
@ -168,12 +170,13 @@ func (s *APITestSuite) TestNodeStartCrash() {
|
|||
defer func() { s.NoError(db.Close()) }()
|
||||
|
||||
// start node outside the manager (on the same port), so that manager node.Start() method fails
|
||||
outsideNode, err := node.MakeNode(nodeConfig, db)
|
||||
outsideNode, err := node.MakeNode(nodeConfig, nil, db)
|
||||
s.NoError(err)
|
||||
err = outsideNode.Start()
|
||||
s.NoError(err)
|
||||
|
||||
// now try starting using node manager, it should fail (error is irrelevant as it is implementation detail)
|
||||
s.NoError(s.backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
|
||||
s.Error(<-api.RunAsync(func() error { return s.backend.StartNode(nodeConfig) }))
|
||||
|
||||
select {
|
||||
|
|
|
@ -25,7 +25,7 @@ func (s *APIBackendTestSuite) TestNetworkSwitching() {
|
|||
// Get test node configuration.
|
||||
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
|
||||
s.NoError(err)
|
||||
|
||||
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
|
||||
s.False(s.Backend.IsNodeRunning())
|
||||
s.Require().NoError(s.Backend.StartNode(nodeConfig))
|
||||
s.True(s.Backend.IsNodeRunning())
|
||||
|
@ -87,6 +87,7 @@ func (s *APIBackendTestSuite) TestRestartNode() {
|
|||
// get config
|
||||
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
|
||||
s.NoError(err)
|
||||
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
|
||||
|
||||
s.False(s.Backend.IsNodeRunning())
|
||||
require.NoError(s.Backend.StartNode(nodeConfig))
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
gethnode "github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
|
@ -71,20 +70,6 @@ func (s *ManagerTestSuite) TestReferencesWithoutStartedNode() {
|
|||
},
|
||||
node.ErrNoRunningNode,
|
||||
},
|
||||
{
|
||||
"non-null manager, no running node, get AccountManager",
|
||||
func() (interface{}, error) {
|
||||
return s.StatusNode.AccountManager()
|
||||
},
|
||||
node.ErrNoGethNode,
|
||||
},
|
||||
{
|
||||
"non-null manager, no running node, get AccountKeyStore",
|
||||
func() (interface{}, error) {
|
||||
return s.StatusNode.AccountKeyStore()
|
||||
},
|
||||
node.ErrNoGethNode,
|
||||
},
|
||||
{
|
||||
"non-null manager, no running node, get RPC Client",
|
||||
func() (interface{}, error) {
|
||||
|
@ -148,13 +133,6 @@ func (s *ManagerTestSuite) TestReferencesWithStartedNode() {
|
|||
},
|
||||
&accounts.Manager{},
|
||||
},
|
||||
{
|
||||
"node is running, get AccountKeyStore",
|
||||
func() (interface{}, error) {
|
||||
return s.StatusNode.AccountKeyStore()
|
||||
},
|
||||
&keystore.KeyStore{},
|
||||
},
|
||||
{
|
||||
"node is running, get RPC Client",
|
||||
func() (interface{}, error) {
|
||||
|
@ -183,12 +161,12 @@ func (s *ManagerTestSuite) TestNodeStartStop() {
|
|||
|
||||
// start node
|
||||
s.False(s.StatusNode.IsRunning())
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
// wait till node is started
|
||||
s.True(s.StatusNode.IsRunning())
|
||||
|
||||
// try starting another node (w/o stopping the previously started node)
|
||||
s.Equal(node.ErrNodeRunning, s.StatusNode.Start(nodeConfig))
|
||||
s.Equal(node.ErrNodeRunning, s.StatusNode.Start(nodeConfig, nil))
|
||||
|
||||
// now stop node
|
||||
time.Sleep(100 * time.Millisecond) //https://github.com/status-im/status-go/issues/429#issuecomment-339663163
|
||||
|
@ -196,7 +174,7 @@ func (s *ManagerTestSuite) TestNodeStartStop() {
|
|||
s.False(s.StatusNode.IsRunning())
|
||||
|
||||
// start new node with exactly the same config
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
s.True(s.StatusNode.IsRunning())
|
||||
|
||||
// finally stop the node
|
||||
|
@ -209,7 +187,7 @@ func (s *ManagerTestSuite) TestNetworkSwitching() {
|
|||
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
|
||||
s.NoError(err)
|
||||
s.False(s.StatusNode.IsRunning())
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
// wait till node is started
|
||||
s.Require().True(s.StatusNode.IsRunning())
|
||||
|
||||
|
@ -225,7 +203,7 @@ func (s *ManagerTestSuite) TestNetworkSwitching() {
|
|||
// start new node with completely different config
|
||||
nodeConfig, err = MakeTestNodeConfig(params.RinkebyNetworkID)
|
||||
s.NoError(err)
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
s.True(s.StatusNode.IsRunning())
|
||||
|
||||
// make sure we are on another network indeed
|
||||
|
@ -251,7 +229,7 @@ func (s *ManagerTestSuite) TestStartWithUpstreamEnabled() {
|
|||
nodeConfig.UpstreamConfig.Enabled = true
|
||||
nodeConfig.UpstreamConfig.URL = networkURL
|
||||
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
s.True(s.StatusNode.IsRunning())
|
||||
|
||||
time.Sleep(100 * time.Millisecond) //https://github.com/status-im/status-go/issues/429#issuecomment-339663163
|
||||
|
|
|
@ -49,7 +49,7 @@ func (s *RPCTestSuite) TestCallRPC() {
|
|||
nodeConfig.UpstreamConfig.URL = networkURL
|
||||
}
|
||||
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
|
||||
rpcClient := s.StatusNode.RPCClient()
|
||||
s.NotNil(rpcClient)
|
||||
|
@ -127,7 +127,7 @@ func (s *RPCTestSuite) TestCallRawResult() {
|
|||
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
|
||||
s.NoError(err)
|
||||
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
|
||||
client := s.StatusNode.RPCPrivateClient()
|
||||
s.NotNil(client)
|
||||
|
@ -145,7 +145,7 @@ func (s *RPCTestSuite) TestCallRawResultGetTransactionReceipt() {
|
|||
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
|
||||
s.NoError(err)
|
||||
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
|
||||
client := s.StatusNode.RPCClient()
|
||||
s.NotNil(client)
|
||||
|
|
|
@ -73,6 +73,7 @@ func (s *BaseJSONRPCSuite) SetupTest(upstreamEnabled, statusServiceEnabled, debu
|
|||
|
||||
nodeConfig, err := utils.MakeTestNodeConfig(utils.GetNetworkID())
|
||||
s.NoError(err)
|
||||
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
|
||||
|
||||
nodeConfig.IPCEnabled = false
|
||||
nodeConfig.EnableStatusService = statusServiceEnabled
|
||||
|
|
|
@ -51,7 +51,7 @@ func (s *StatusNodeTestSuite) StartTestNode(opts ...TestNodeOption) {
|
|||
s.NoError(importTestAccounts(nodeConfig.KeyStoreDir))
|
||||
|
||||
s.False(s.StatusNode.IsRunning())
|
||||
s.NoError(s.StatusNode.Start(nodeConfig))
|
||||
s.NoError(s.StatusNode.Start(nodeConfig, nil))
|
||||
s.True(s.StatusNode.IsRunning())
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ func (s *BackendTestSuite) StartTestBackend(opts ...TestNodeOption) {
|
|||
for i := range opts {
|
||||
opts[i](nodeConfig)
|
||||
}
|
||||
|
||||
s.NoError(s.Backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
|
||||
// import account keys
|
||||
s.NoError(importTestAccounts(nodeConfig.KeyStoreDir))
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ func (s *WhisperExtensionSuite) SetupTest() {
|
|||
cfg, err := utils.MakeTestNodeConfigWithDataDir(fmt.Sprintf("test-shhext-%d", i), dir, 777)
|
||||
s.Require().NoError(err)
|
||||
s.nodes[i] = node.New()
|
||||
s.Require().NoError(s.nodes[i].Start(cfg))
|
||||
s.Require().NoError(s.nodes[i].Start(cfg, nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -708,7 +708,10 @@ func (s *WhisperMailboxSuite) startBackend(name string) (*api.StatusBackend, fun
|
|||
backend := api.NewStatusBackend()
|
||||
nodeConfig, err := utils.MakeTestNodeConfig(utils.GetNetworkID())
|
||||
nodeConfig.DataDir = datadir
|
||||
nodeConfig.KeyStoreDir = filepath.Join(datadir, "keystore")
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(backend.AccountManager().InitKeystore(nodeConfig.KeyStoreDir))
|
||||
|
||||
s.Require().False(backend.IsNodeRunning())
|
||||
|
||||
nodeConfig.WhisperConfig.LightClient = true
|
||||
|
@ -748,6 +751,7 @@ func (s *WhisperMailboxSuite) startMailboxBackendWithCallback(
|
|||
s.Require().NoError(err)
|
||||
|
||||
mailboxBackend := api.NewStatusBackend()
|
||||
s.Require().NoError(mailboxBackend.AccountManager().InitKeystore(mailboxConfig.KeyStoreDir))
|
||||
datadir := filepath.Join(utils.RootDir, ".ethereumtest/mailbox", name)
|
||||
|
||||
mailboxConfig.LightEthConfig.Enabled = false
|
||||
|
|
|
@ -40,7 +40,7 @@ func (s *WhisperTestSuite) TestWhisperFilterRace() {
|
|||
whisperService, err := s.Backend.StatusNode().WhisperService()
|
||||
s.NoError(err)
|
||||
|
||||
accountManager := account.NewManager(s.Backend.StatusNode())
|
||||
accountManager := s.Backend.AccountManager()
|
||||
s.NotNil(accountManager)
|
||||
|
||||
whisperAPI := whisper.NewPublicWhisperAPI(whisperService)
|
||||
|
|
Loading…
Reference in New Issue