diff --git a/constants/global_constants.go b/constants/global_constants.go new file mode 100644 index 000000000..3917788c7 --- /dev/null +++ b/constants/global_constants.go @@ -0,0 +1,8 @@ +package constants + +const ( + MaxNumberOfAccounts = 20 + MaxNumberOfKeypairs = 5 // including the profile keypair + MaxNumberOfWatchOnlyAccounts = 3 + MaxNumberOfSavedAddresses = 20 +) diff --git a/protocol/messenger_saved_address.go b/protocol/messenger_saved_address.go index 71689ac42..20afa8221 100644 --- a/protocol/messenger_saved_address.go +++ b/protocol/messenger_saved_address.go @@ -36,6 +36,14 @@ func (m *Messenger) GetSavedAddresses(ctx context.Context) ([]*wallet.SavedAddre return m.savedAddressesManager.GetSavedAddresses() } +func (m *Messenger) GetSavedAddressesPerMode(isTest bool) ([]*wallet.SavedAddress, error) { + return m.savedAddressesManager.GetSavedAddressesPerMode(isTest) +} + +func (m *Messenger) RemainingCapacityForSavedAddresses(testnetMode bool) (int, error) { + return m.savedAddressesManager.RemainingCapacityForSavedAddresses(testnetMode) +} + func (m *Messenger) garbageCollectRemovedSavedAddresses() error { return m.savedAddressesManager.DeleteSoftRemovedSavedAddresses(uint64(time.Now().AddDate(0, 0, -30).Unix())) } diff --git a/protocol/messenger_wallet.go b/protocol/messenger_wallet.go index 6a15e7c68..9ee3f927e 100644 --- a/protocol/messenger_wallet.go +++ b/protocol/messenger_wallet.go @@ -10,6 +10,7 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/status-im/status-go/account" + "github.com/status-im/status-go/constants" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/multiaccounts/accounts" walletsettings "github.com/status-im/status-go/multiaccounts/settings_wallet" @@ -781,3 +782,45 @@ func (m *Messenger) resolveAndSyncKeypairOrJustWalletAccount(keyUID string, addr chat.LastClockValue = clock return m.saveChat(chat) } + +func (m *Messenger) RemainingAccountCapacity() (int, error) { + accounts, err := m.settings.GetActiveAccounts() + if err != nil { + return 0, err + } + numOfAccountsWithoutChatAccount := 0 + if len(accounts) > 0 { + numOfAccountsWithoutChatAccount = len(accounts) - 1 + } + remainingCapacity := constants.MaxNumberOfAccounts - numOfAccountsWithoutChatAccount + if remainingCapacity <= 0 { + return 0, errors.New("no more accounts can be added") + } + + return remainingCapacity, nil +} + +func (m *Messenger) RemainingKeypairCapacity() (int, error) { + keypairs, err := m.settings.GetActiveKeypairs() + if err != nil { + return 0, err + } + remainingCapacity := constants.MaxNumberOfKeypairs - len(keypairs) + if remainingCapacity <= 0 { + return 0, errors.New("no more keypairs can be added") + } + + return remainingCapacity, nil +} + +func (m *Messenger) RemainingWatchOnlyAccountCapacity() (int, error) { + accounts, err := m.settings.GetActiveWatchOnlyAccounts() + if err != nil { + return 0, err + } + remainingCapacity := constants.MaxNumberOfWatchOnlyAccounts - len(accounts) + if remainingCapacity <= 0 { + return 0, errors.New("no more watch-only accounts can be added") + } + return remainingCapacity, nil +} diff --git a/protocol/messenger_wallet_test.go b/protocol/messenger_wallet_test.go new file mode 100644 index 000000000..61d3d780f --- /dev/null +++ b/protocol/messenger_wallet_test.go @@ -0,0 +1,193 @@ +package protocol + +import ( + "testing" + + "github.com/status-im/status-go/constants" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/multiaccounts/accounts" + + "github.com/stretchr/testify/suite" +) + +func TestWalletSuite(t *testing.T) { + suite.Run(t, new(WalletSuite)) +} + +type WalletSuite struct { + MessengerBaseTestSuite +} + +func (s *WalletSuite) TestRemainingCapacity() { + profileKeypair := accounts.GetProfileKeypairForTest(true, true, true) + seedImportedKeypair := accounts.GetSeedImportedKeypair1ForTest() + woAccounts := accounts.GetWatchOnlyAccountsForTest() + + // Empty DB + capacity, err := s.m.RemainingAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfAccounts, capacity) + + capacity, err = s.m.RemainingKeypairCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfKeypairs, capacity) + + capacity, err = s.m.RemainingWatchOnlyAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfWatchOnlyAccounts, capacity) + + // profile keypair with chat account, default wallet account and 2 more derived accounts added + err = s.m.SaveOrUpdateKeypair(profileKeypair) + s.Require().NoError(err) + + capacity, err = s.m.RemainingAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfAccounts-3, capacity) + + capacity, err = s.m.RemainingKeypairCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfKeypairs-1, capacity) + + capacity, err = s.m.RemainingWatchOnlyAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfWatchOnlyAccounts, capacity) + + // seed keypair with 2 derived accounts added + err = s.m.SaveOrUpdateKeypair(seedImportedKeypair) + s.Require().NoError(err) + + capacity, err = s.m.RemainingAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfAccounts-(3+2), capacity) + + capacity, err = s.m.RemainingKeypairCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfKeypairs-(1+1), capacity) + + capacity, err = s.m.RemainingWatchOnlyAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfWatchOnlyAccounts, capacity) + + // 1 Watch only accounts added + err = s.m.SaveOrUpdateAccount(woAccounts[0]) + s.Require().NoError(err) + + capacity, err = s.m.RemainingAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfAccounts-(3+2+1), capacity) + + capacity, err = s.m.RemainingKeypairCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfKeypairs-(1+1), capacity) + + capacity, err = s.m.RemainingWatchOnlyAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfWatchOnlyAccounts-1, capacity) + + // try to add 3 more keypairs + seedImportedKeypair2 := accounts.GetSeedImportedKeypair2ForTest() + seedImportedKeypair2.KeyUID = "0000000000000000000000000000000000000000000000000000000000000091" + seedImportedKeypair2.Accounts[0].Address = types.Address{0x91} + seedImportedKeypair2.Accounts[0].KeyUID = seedImportedKeypair2.KeyUID + seedImportedKeypair2.Accounts[1].Address = types.Address{0x92} + seedImportedKeypair2.Accounts[1].KeyUID = seedImportedKeypair2.KeyUID + + err = s.m.SaveOrUpdateKeypair(seedImportedKeypair2) + s.Require().NoError(err) + + seedImportedKeypair3 := accounts.GetSeedImportedKeypair2ForTest() + seedImportedKeypair3.KeyUID = "0000000000000000000000000000000000000000000000000000000000000093" + seedImportedKeypair3.Accounts[0].Address = types.Address{0x93} + seedImportedKeypair3.Accounts[0].KeyUID = seedImportedKeypair3.KeyUID + seedImportedKeypair3.Accounts[1].Address = types.Address{0x94} + seedImportedKeypair3.Accounts[1].KeyUID = seedImportedKeypair3.KeyUID + + err = s.m.SaveOrUpdateKeypair(seedImportedKeypair3) + s.Require().NoError(err) + + seedImportedKeypair4 := accounts.GetSeedImportedKeypair2ForTest() + seedImportedKeypair4.KeyUID = "0000000000000000000000000000000000000000000000000000000000000095" + seedImportedKeypair4.Accounts[0].Address = types.Address{0x95} + seedImportedKeypair4.Accounts[0].KeyUID = seedImportedKeypair4.KeyUID + seedImportedKeypair4.Accounts[1].Address = types.Address{0x96} + seedImportedKeypair4.Accounts[1].KeyUID = seedImportedKeypair4.KeyUID + + err = s.m.SaveOrUpdateKeypair(seedImportedKeypair4) + s.Require().NoError(err) + + // check the capacity after adding 3 more keypairs + capacity, err = s.m.RemainingAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfAccounts-(3+2+1+3*2), capacity) + + capacity, err = s.m.RemainingKeypairCapacity() + s.Require().Error(err) + s.Require().Equal("no more keypairs can be added", err.Error()) + s.Require().Equal(0, capacity) + + capacity, err = s.m.RemainingWatchOnlyAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfWatchOnlyAccounts-1, capacity) + + // add 2 more watch only accounts + err = s.m.SaveOrUpdateAccount(woAccounts[1]) + s.Require().NoError(err) + err = s.m.SaveOrUpdateAccount(woAccounts[2]) + s.Require().NoError(err) + + // check the capacity after adding 8 more watch only accounts + capacity, err = s.m.RemainingAccountCapacity() + s.Require().NoError(err) + s.Require().Equal(constants.MaxNumberOfAccounts-(3+2+3+3*2), capacity) + + capacity, err = s.m.RemainingKeypairCapacity() + s.Require().Error(err) + s.Require().Equal("no more keypairs can be added", err.Error()) + s.Require().Equal(0, capacity) + + capacity, err = s.m.RemainingWatchOnlyAccountCapacity() + s.Require().Error(err) + s.Require().Equal("no more watch-only accounts can be added", err.Error()) + s.Require().Equal(0, capacity) + + // add 6 accounts more + seedImportedKeypair4.Accounts[0].Address = types.Address{0x81} + err = s.m.SaveOrUpdateAccount(seedImportedKeypair4.Accounts[0]) + s.Require().NoError(err) + + seedImportedKeypair4.Accounts[0].Address = types.Address{0x82} + err = s.m.SaveOrUpdateAccount(seedImportedKeypair4.Accounts[0]) + s.Require().NoError(err) + + seedImportedKeypair4.Accounts[0].Address = types.Address{0x83} + err = s.m.SaveOrUpdateAccount(seedImportedKeypair4.Accounts[0]) + s.Require().NoError(err) + + seedImportedKeypair4.Accounts[0].Address = types.Address{0x84} + err = s.m.SaveOrUpdateAccount(seedImportedKeypair4.Accounts[0]) + s.Require().NoError(err) + + seedImportedKeypair4.Accounts[0].Address = types.Address{0x85} + err = s.m.SaveOrUpdateAccount(seedImportedKeypair4.Accounts[0]) + s.Require().NoError(err) + + seedImportedKeypair4.Accounts[0].Address = types.Address{0x86} + err = s.m.SaveOrUpdateAccount(seedImportedKeypair4.Accounts[0]) + s.Require().NoError(err) + + // check the capacity after adding 8 more watch only accounts + capacity, err = s.m.RemainingAccountCapacity() + s.Require().Error(err) + s.Require().Equal("no more accounts can be added", err.Error()) + s.Require().Equal(0, capacity) + + capacity, err = s.m.RemainingKeypairCapacity() + s.Require().Error(err) + s.Require().Equal("no more keypairs can be added", err.Error()) + s.Require().Equal(0, capacity) + + capacity, err = s.m.RemainingWatchOnlyAccountCapacity() + s.Require().Error(err) + s.Require().Equal("no more watch-only accounts can be added", err.Error()) + s.Require().Equal(0, capacity) +} diff --git a/services/accounts/accounts.go b/services/accounts/accounts.go index 5a538f9cf..bb9f1d86c 100644 --- a/services/accounts/accounts.go +++ b/services/accounts/accounts.go @@ -211,6 +211,21 @@ func (api *API) AddKeypair(ctx context.Context, password string, keypair *accoun return nil } +// RemainingAccountCapacity returns the number of accounts that can be added. +func (api *API) RemainingAccountCapacity(ctx context.Context) (int, error) { + return (*api.messenger).RemainingAccountCapacity() +} + +// RemainingKeypairCapacity returns the number of keypairs that can be added. +func (api *API) RemainingKeypairCapacity(ctx context.Context) (int, error) { + return (*api.messenger).RemainingKeypairCapacity() +} + +// RemainingWatchOnlyAccountCapacity returns the number of watch-only accounts that can be added. +func (api *API) RemainingWatchOnlyAccountCapacity(ctx context.Context) (int, error) { + return (*api.messenger).RemainingWatchOnlyAccountCapacity() +} + func (api *API) checkAccountValidity(account *accounts.Account) error { if len(account.Address) == 0 { return errors.New("`Address` field of an account must be set") diff --git a/services/ext/api.go b/services/ext/api.go index ed8dc7c57..9f69907da 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -1126,6 +1126,15 @@ func (api *PublicAPI) GetSavedAddresses(ctx context.Context) ([]*wallet.SavedAdd return api.service.messenger.GetSavedAddresses(ctx) } +func (api *PublicAPI) GetSavedAddressesPerMode(ctx context.Context, testnetMode bool) ([]*wallet.SavedAddress, error) { + return api.service.messenger.GetSavedAddressesPerMode(testnetMode) +} + +// RemainingCapacityForSavedAddresses returns the number of saved addresses that can be added +func (api *PublicAPI) RemainingCapacityForSavedAddresses(ctx context.Context, testnetMode bool) (int, error) { + return api.service.messenger.RemainingCapacityForSavedAddresses(testnetMode) +} + // PushNotifications server endpoints func (api *PublicAPI) StartPushNotificationsServer() error { err := api.service.accountsDB.SaveSettingField(settings.PushNotificationsServerEnabled, true) diff --git a/services/wallet/saved_addresses.go b/services/wallet/saved_addresses.go index 02a6e487d..52eabeea6 100644 --- a/services/wallet/saved_addresses.go +++ b/services/wallet/saved_addresses.go @@ -3,10 +3,12 @@ package wallet import ( "database/sql" "encoding/json" + "errors" "fmt" "time" "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/constants" multiAccCommon "github.com/status-im/status-go/multiaccounts/common" ) @@ -115,11 +117,28 @@ func (sam *SavedAddressesManager) GetSavedAddresses() ([]*SavedAddress, error) { return sam.getSavedAddresses("removed != 1") } +func (sam *SavedAddressesManager) GetSavedAddressesPerMode(testnetMode bool) ([]*SavedAddress, error) { + return sam.getSavedAddresses(fmt.Sprintf("is_test = %t AND removed != 1", testnetMode)) +} + // GetRawSavedAddresses provides access to the soft-delete and sync metadata func (sam *SavedAddressesManager) GetRawSavedAddresses() ([]*SavedAddress, error) { return sam.getSavedAddresses("") } +func (sam *SavedAddressesManager) RemainingCapacityForSavedAddresses(testnetMode bool) (int, error) { + savedAddress, err := sam.GetSavedAddressesPerMode(testnetMode) + if err != nil { + return 0, err + } + remainingCapacity := constants.MaxNumberOfSavedAddresses - len(savedAddress) + if remainingCapacity <= 0 { + return 0, errors.New("no more save addresses can be added") + } + + return remainingCapacity, nil +} + func (sam *SavedAddressesManager) upsertSavedAddress(sa SavedAddress, tx *sql.Tx) (err error) { if tx == nil { tx, err = sam.db.Begin() diff --git a/services/wallet/saved_addresses_test.go b/services/wallet/saved_addresses_test.go index c0d53f6db..939d86bc2 100644 --- a/services/wallet/saved_addresses_test.go +++ b/services/wallet/saved_addresses_test.go @@ -394,3 +394,145 @@ func TestSavedAddressesAddSame(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(rst)) } + +func TestSavedAddressesPerTestnetMode(t *testing.T) { + manager, stop := setupTestSavedAddressesDB(t) + defer stop() + + addresses := []SavedAddress{ + SavedAddress{ + Address: common.Address{1}, + Name: "addr1", + IsTest: true, + }, + SavedAddress{ + Address: common.Address{2}, + Name: "addr2", + IsTest: false, + }, + SavedAddress{ + Address: common.Address{3}, + Name: "addr3", + IsTest: true, + }, + SavedAddress{ + Address: common.Address{4}, + Name: "addr4", + IsTest: false, + }, + SavedAddress{ + Address: common.Address{5}, + Name: "addr5", + IsTest: true, + Removed: true, + }, + SavedAddress{ + Address: common.Address{6}, + Name: "addr6", + IsTest: false, + Removed: true, + }, + SavedAddress{ + Address: common.Address{7}, + Name: "addr7", + IsTest: true, + }, + } + + for _, sa := range addresses { + err := manager.upsertSavedAddress(sa, nil) + require.NoError(t, err) + } + + res, err := manager.GetSavedAddresses() + require.NoError(t, err) + require.Equal(t, len(res), len(addresses)-2) + + res, err = manager.GetSavedAddressesPerMode(true) + require.NoError(t, err) + require.Equal(t, len(res), 3) + + res, err = manager.GetSavedAddressesPerMode(false) + require.NoError(t, err) + require.Equal(t, len(res), 2) +} + +func TestSavedAddressesCapacity(t *testing.T) { + manager, stop := setupTestSavedAddressesDB(t) + defer stop() + + addresses := []SavedAddress{ + SavedAddress{ + Address: common.Address{1}, + Name: "addr1", + IsTest: true, + }, + SavedAddress{ + Address: common.Address{2}, + Name: "addr2", + IsTest: false, + }, + SavedAddress{ + Address: common.Address{3}, + Name: "addr3", + IsTest: true, + }, + SavedAddress{ + Address: common.Address{4}, + Name: "addr4", + IsTest: false, + }, + SavedAddress{ + Address: common.Address{5}, + Name: "addr5", + IsTest: true, + Removed: true, + }, + SavedAddress{ + Address: common.Address{6}, + Name: "addr6", + IsTest: false, + Removed: true, + }, + SavedAddress{ + Address: common.Address{7}, + Name: "addr7", + IsTest: true, + }, + } + + for _, sa := range addresses { + err := manager.upsertSavedAddress(sa, nil) + require.NoError(t, err) + } + + capacity, err := manager.RemainingCapacityForSavedAddresses(true) + require.NoError(t, err) + require.Equal(t, 17, capacity) + + capacity, err = manager.RemainingCapacityForSavedAddresses(false) + require.NoError(t, err) + require.Equal(t, 18, capacity) + + // add 17 more for testnet and 18 more for mainnet mode + for i := 1; i < 36; i++ { + sa := SavedAddress{ + Address: common.Address{byte(i + 8)}, + Name: "addr" + strconv.Itoa(i+8), + IsTest: i%2 == 0, + } + + err := manager.upsertSavedAddress(sa, nil) + require.NoError(t, err) + } + + capacity, err = manager.RemainingCapacityForSavedAddresses(true) + require.Error(t, err) + require.Equal(t, "no more save addresses can be added", err.Error()) + require.Equal(t, 0, capacity) + + capacity, err = manager.RemainingCapacityForSavedAddresses(false) + require.Error(t, err) + require.Equal(t, "no more save addresses can be added", err.Error()) + require.Equal(t, 0, capacity) +}