Fix tests and lint
This commit is contained in:
parent
2695b70159
commit
c78e084eb9
1
Makefile
1
Makefile
|
@ -300,7 +300,6 @@ test-e2e: ##@tests Run e2e tests
|
|||
go test -timeout 5m ./t/e2e/api/... -network=$(networkid) $(gotest_extraflags)
|
||||
go test -timeout 5m ./t/e2e/node/... -network=$(networkid) $(gotest_extraflags)
|
||||
go test -timeout 20m ./t/e2e/rpc/... -network=$(networkid) $(gotest_extraflags)
|
||||
go test -timeout 20m ./t/e2e/whisper/... -network=$(networkid) $(gotest_extraflags)
|
||||
go test -timeout 10m ./t/e2e/transactions/... -network=$(networkid) $(gotest_extraflags)
|
||||
go test -timeout 10m ./t/e2e/services/... -network=$(networkid) $(gotest_extraflags)
|
||||
|
||||
|
|
|
@ -76,8 +76,8 @@ type IdentifiedAccountInfo struct {
|
|||
|
||||
func (iai *IdentifiedAccountInfo) ToMultiAccount() *multiaccounts.Account {
|
||||
return &multiaccounts.Account{
|
||||
Timestamp: time.Now().Unix(),
|
||||
KeyUID: iai.KeyUID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
KeyUID: iai.KeyUID,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,10 @@ var (
|
|||
)
|
||||
|
||||
type LoginParams struct {
|
||||
ChatAddress types.Address `json:"chatAddress"`
|
||||
Password string `json:"password"`
|
||||
MainAccount types.Address `json:"mainAccount"`
|
||||
WatchAddresses []types.Address `json:"watchAddresses"`
|
||||
ChatAddress types.Address `json:"chatAddress"`
|
||||
Password string `json:"password"`
|
||||
MainAccount types.Address `json:"mainAccount"`
|
||||
WatchAddresses []types.Address `json:"watchAddresses"`
|
||||
MultiAccount *multiaccounts.Account `json:"multiAccount"`
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ type StatusBackend interface {
|
|||
ConnectionChange(typ string, expensive bool)
|
||||
AppStateChange(state string)
|
||||
|
||||
InjectChatAccount(chatKeyHex, encryptionKeyHex string) error // NOTE: Only used in lib and in tests
|
||||
ExtractGroupMembershipSignatures(signaturePairs [][2]string) ([]string, error)
|
||||
SignGroupMembership(content string) (string, error)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
|
@ -13,6 +12,8 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
@ -184,8 +185,8 @@ func TestBackendAccountsConcurrently(t *testing.T) {
|
|||
count := 3
|
||||
type AccountData struct {
|
||||
MasterAccount generator.GeneratedAccountInfo
|
||||
AccountInfo account.Info
|
||||
Password string
|
||||
AccountInfo account.Info
|
||||
Password string
|
||||
}
|
||||
addressCh := make(chan AccountData, count) // use buffered channel to avoid blocking
|
||||
|
||||
|
@ -210,9 +211,9 @@ func TestBackendAccountsConcurrently(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func(accountData AccountData) {
|
||||
loginParams := account.LoginParams{
|
||||
MainAccount: types.HexToAddress(accountData.AccountInfo.WalletAddress),
|
||||
ChatAddress: types.HexToAddress(accountData.AccountInfo.ChatAddress),
|
||||
Password: accountData.Password,
|
||||
MainAccount: types.HexToAddress(accountData.AccountInfo.WalletAddress),
|
||||
ChatAddress: types.HexToAddress(accountData.AccountInfo.ChatAddress),
|
||||
Password: accountData.Password,
|
||||
MultiAccount: accountData.MasterAccount.ToMultiAccount(),
|
||||
}
|
||||
assert.NoError(t, backend.SelectAccount(loginParams))
|
||||
|
@ -229,53 +230,6 @@ func TestBackendAccountsConcurrently(t *testing.T) {
|
|||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestBackendInjectChatAccount(t *testing.T) {
|
||||
utils.Init()
|
||||
|
||||
backend := NewGethStatusBackend()
|
||||
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())
|
||||
}()
|
||||
|
||||
backend.account = &multiaccounts.Account{KeyUID: "0xdeadbeef"}
|
||||
|
||||
chatPrivKey, err := gethcrypto.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
encryptionPrivKey, err := gethcrypto.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
chatPrivKeyHex := hex.EncodeToString(gethcrypto.FromECDSA(chatPrivKey))
|
||||
chatPubKeyHex := types.EncodeHex(gethcrypto.FromECDSAPub(&chatPrivKey.PublicKey))
|
||||
encryptionPrivKeyHex := hex.EncodeToString(gethcrypto.FromECDSA(encryptionPrivKey))
|
||||
|
||||
whisperService, err := backend.StatusNode().WhisperService()
|
||||
require.NoError(t, err)
|
||||
|
||||
// public key should not be already in whisper
|
||||
require.False(t, whisperService.HasKeyPair(chatPubKeyHex), "identity already present in whisper")
|
||||
|
||||
// call InjectChatAccount
|
||||
require.NoError(t, backend.InjectChatAccount(chatPrivKeyHex, encryptionPrivKeyHex))
|
||||
|
||||
// public key should now be in whisper
|
||||
require.True(t, whisperService.HasKeyPair(chatPubKeyHex), "identity not injected into whisper")
|
||||
|
||||
// wallet account should not be selected
|
||||
mainAccountAddress, err := backend.AccountManager().MainAccountAddress()
|
||||
require.Equal(t, types.Address{}, mainAccountAddress)
|
||||
require.Equal(t, account.ErrNoAccountSelected, err)
|
||||
|
||||
// selected chat account should have the key injected previously
|
||||
chatAcc, err := backend.AccountManager().SelectedChatAccount()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, chatPrivKey, chatAcc.AccountKey.PrivateKey)
|
||||
}
|
||||
|
||||
func TestBackendConnectionChangesConcurrently(t *testing.T) {
|
||||
connections := [...]string{wifi, cellular, unknown}
|
||||
backend := NewGethStatusBackend()
|
||||
|
@ -442,48 +396,6 @@ func TestStartStopMultipleTimes(t *testing.T) {
|
|||
require.NoError(t, backend.StopNode())
|
||||
}
|
||||
|
||||
func TestSignHash(t *testing.T) {
|
||||
utils.Init()
|
||||
|
||||
backend := NewGethStatusBackend()
|
||||
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() {
|
||||
require.NoError(t, backend.StopNode())
|
||||
}()
|
||||
|
||||
backend.account = &multiaccounts.Account{KeyUID: "0xdeadbeef"}
|
||||
|
||||
var testCases = []struct {
|
||||
name string
|
||||
chatPrivKeyHex string
|
||||
hashHex string
|
||||
expectedSignatureHex string
|
||||
}{
|
||||
{
|
||||
name: "tc1",
|
||||
chatPrivKeyHex: "facadefacadefacadefacadefacadefacadefacadefacadefacadefacadefaca",
|
||||
hashHex: "0xa4735de5193362fe856416000105cdfa6ce56265607311cebae93b26e5adf438",
|
||||
expectedSignatureHex: "0x176c971bae188c663614fc535ac9dbf62871dfeaadb38645809a510d28b3c4b0245415d5547c1b27f7cfea3341564f9c6981421144d3606b455346be69bd078c01",
|
||||
},
|
||||
}
|
||||
|
||||
const dummyEncKey = "facadefacadefacadefacadefacadefacadefacadefacadefacadefacadefaca"
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.NoError(t, backend.InjectChatAccount(tc.chatPrivKeyHex, dummyEncKey))
|
||||
|
||||
signature, err := backend.SignHash(tc.hashHex)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, signature, tc.expectedSignatureHex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashTypedData(t *testing.T) {
|
||||
utils.Init()
|
||||
|
||||
|
|
|
@ -1230,23 +1230,6 @@ func (b *GethStatusBackend) startWallet(watchNewBlocks bool) error {
|
|||
watchNewBlocks)
|
||||
}
|
||||
|
||||
// InjectChatAccount selects the current chat account using chatKeyHex and injects the key into whisper.
|
||||
// TODO: change the interface and omit the last argument.
|
||||
func (b *GethStatusBackend) InjectChatAccount(chatKeyHex, _ string) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.accountManager.Logout()
|
||||
|
||||
chatKey, err := ethcrypto.HexToECDSA(chatKeyHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.accountManager.SetChatAccount(chatKey)
|
||||
|
||||
return b.injectAccountsIntoServices()
|
||||
}
|
||||
|
||||
func appendIf(condition bool, services []gethnode.ServiceConstructor, service gethnode.ServiceConstructor) []gethnode.ServiceConstructor {
|
||||
if !condition {
|
||||
return services
|
||||
|
|
|
@ -928,23 +928,6 @@ func (b *nimbusStatusBackend) injectAccountIntoServices() error {
|
|||
// )
|
||||
// }
|
||||
|
||||
// InjectChatAccount selects the current chat account using chatKeyHex and injects the key into whisper.
|
||||
// TODO: change the interface and omit the last argument.
|
||||
func (b *nimbusStatusBackend) InjectChatAccount(chatKeyHex, _ string) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.accountManager.Logout()
|
||||
|
||||
chatKey, err := crypto.HexToECDSA(chatKeyHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.accountManager.SetChatAccount(chatKey)
|
||||
|
||||
return b.injectAccountIntoServices()
|
||||
}
|
||||
|
||||
func appendIf(condition bool, services []nimbussvc.ServiceConstructor, service nimbussvc.ServiceConstructor) []nimbussvc.ServiceConstructor {
|
||||
if !condition {
|
||||
return services
|
||||
|
|
|
@ -118,7 +118,7 @@ func TestDatabase_GetIdentityImage(t *testing.T) {
|
|||
{
|
||||
keyUID2,
|
||||
images.LargeDimName,
|
||||
`{"keyUid":"","type":"","uri":"","width":0,"height":0,"fileSize":0,"resizeTarget":0}`,
|
||||
"null",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -161,7 +161,7 @@ func TestDatabase_GetAccountsWithIdentityImages(t *testing.T) {
|
|||
}
|
||||
|
||||
seedTestDBWithIdentityImages(t, db, keyUID)
|
||||
seedTestDBWithIdentityImages(t, db, keyUID2 + "3")
|
||||
seedTestDBWithIdentityImages(t, db, keyUID2+"3")
|
||||
|
||||
err := db.UpdateAccountTimestamp(keyUID, 100)
|
||||
require.NoError(t, err)
|
||||
|
@ -171,8 +171,8 @@ func TestDatabase_GetAccountsWithIdentityImages(t *testing.T) {
|
|||
accs, err := db.GetAccounts()
|
||||
require.NoError(t, err)
|
||||
|
||||
accJson, err := json.Marshal(accs)
|
||||
accJSON, err := json.Marshal(accs)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Exactly(t, expected, string(accJson))
|
||||
require.Exactly(t, expected, string(accJSON))
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"context"
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
"testing"
|
||||
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@ func TestInitProtocol(t *testing.T) {
|
|||
multiAccounts, err := multiaccounts.InitializeDB(tmpfile.Name())
|
||||
require.NoError(t, err)
|
||||
|
||||
acc := &multiaccounts.Account{ KeyUID: "0xdeadbeef" }
|
||||
acc := &multiaccounts.Account{KeyUID: "0xdeadbeef"}
|
||||
|
||||
err = service.InitProtocol(privateKey, sqlDB, multiAccounts, acc, zap.NewNop())
|
||||
require.NoError(t, err)
|
||||
|
@ -198,7 +198,7 @@ func (s *ShhExtSuite) createAndAddNode() {
|
|||
privateKey, err := crypto.GenerateKey()
|
||||
s.NoError(err)
|
||||
|
||||
acc := &multiaccounts.Account{ KeyUID: "0xdeadbeef" }
|
||||
acc := &multiaccounts.Account{KeyUID: "0xdeadbeef"}
|
||||
|
||||
err = service.InitProtocol(privateKey, sqlDB, multiAccounts, acc, zap.NewNop())
|
||||
s.NoError(err)
|
||||
|
|
|
@ -24,7 +24,10 @@ func (s *AccountsTestSuite) TestRPCEthAccounts() {
|
|||
defer s.StopTestBackend()
|
||||
|
||||
// log into test account
|
||||
err := s.Backend.SelectAccount(buildLoginParams(TestConfig.Account1.WalletAddress, TestConfig.Account1.ChatAddress, TestConfig.Account1.Password, nil))
|
||||
loginParams, err := buildLoginParams(TestConfig.Account1.WalletAddress, TestConfig.Account1.ChatAddress, TestConfig.Account1.Password, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.Backend.SelectAccount(loginParams)
|
||||
s.NoError(err)
|
||||
|
||||
rpcClient := s.Backend.StatusNode().RPCClient()
|
||||
|
@ -52,7 +55,10 @@ func (s *AccountsTestSuite) TestRPCEthAccountsWithUpstream() {
|
|||
defer s.StopTestBackend()
|
||||
|
||||
// log into test account
|
||||
err = s.Backend.SelectAccount(buildLoginParams(TestConfig.Account1.WalletAddress, TestConfig.Account1.ChatAddress, TestConfig.Account1.Password, nil))
|
||||
loginParams, err := buildLoginParams(TestConfig.Account1.WalletAddress, TestConfig.Account1.ChatAddress, TestConfig.Account1.Password, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.Backend.SelectAccount(loginParams)
|
||||
s.NoError(err)
|
||||
|
||||
rpcClient := s.Backend.StatusNode().RPCClient()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package accounts
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
@ -11,19 +10,29 @@ import (
|
|||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/extkeys"
|
||||
"github.com/status-im/status-go/t/e2e"
|
||||
"github.com/status-im/status-go/t/utils"
|
||||
)
|
||||
|
||||
func buildLoginParams(mainAccountAddress, chatAddress, password string, watchAddresses []types.Address) account.LoginParams {
|
||||
func buildLoginParams(mainAccountAddress, chatAddress, password string, watchAddresses []types.Address) (account.LoginParams, error) {
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
return account.LoginParams{}, err
|
||||
}
|
||||
|
||||
acc := generator.NewAccount(privateKey, nil)
|
||||
iai := acc.ToIdentifiedAccountInfo("")
|
||||
|
||||
return account.LoginParams{
|
||||
ChatAddress: types.HexToAddress(chatAddress),
|
||||
Password: password,
|
||||
MainAccount: types.HexToAddress(mainAccountAddress),
|
||||
WatchAddresses: watchAddresses,
|
||||
}
|
||||
MultiAccount: iai.ToMultiAccount(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestAccountsTestSuite(t *testing.T) {
|
||||
|
@ -54,15 +63,17 @@ func (s *AccountsTestSuite) TestAccountsList() {
|
|||
s.NoError(err)
|
||||
s.Zero(len(accounts), "accounts returned, while there should be none (we haven't logged in yet)")
|
||||
|
||||
// select account (sub-accounts will be created for this key)
|
||||
err = s.Backend.SelectAccount(
|
||||
buildLoginParams(
|
||||
accountInfo.WalletAddress,
|
||||
accountInfo.ChatAddress,
|
||||
utils.TestConfig.Account1.Password,
|
||||
nil,
|
||||
),
|
||||
loginParams, err := buildLoginParams(
|
||||
accountInfo.WalletAddress,
|
||||
accountInfo.ChatAddress,
|
||||
utils.TestConfig.Account1.Password,
|
||||
nil,
|
||||
)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
// select account (sub-accounts will be created for this key)
|
||||
err = s.Backend.SelectAccount(loginParams)
|
||||
s.NoError(err, "account selection failed")
|
||||
|
||||
// at this point main account should show up
|
||||
|
@ -182,36 +193,24 @@ func (s *AccountsTestSuite) TestSelectAccount() {
|
|||
s.T().Logf("Account created: {walletAddress: %s, walletKey: %s, chatAddress: %s, chatKey: %s}",
|
||||
accountInfo2.WalletAddress, accountInfo2.WalletPubKey, accountInfo2.ChatAddress, accountInfo2.ChatPubKey)
|
||||
|
||||
loginParams, err := buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, "wrongPassword", nil)
|
||||
s.Require().NoError(err)
|
||||
// try selecting with wrong password
|
||||
err = s.Backend.SelectAccount(buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, "wrongPassword", nil))
|
||||
err = s.Backend.SelectAccount(loginParams)
|
||||
expectedErr := errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given password")
|
||||
s.EqualError(expectedErr, err.Error(), "select account is expected to throw error: wrong password used")
|
||||
|
||||
err = s.Backend.SelectAccount(buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, utils.TestConfig.Account1.Password, nil))
|
||||
loginParams, err = buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, utils.TestConfig.Account1.Password, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.Backend.SelectAccount(loginParams)
|
||||
s.NoError(err)
|
||||
|
||||
// select another account, make sure that previous account is wiped out from Whisper cache
|
||||
s.NoError(s.Backend.SelectAccount(buildLoginParams(accountInfo2.WalletAddress, accountInfo2.ChatAddress, utils.TestConfig.Account1.Password, nil)))
|
||||
}
|
||||
|
||||
func (s *AccountsTestSuite) TestSetChatAccount() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
chatPrivKey, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
chatPrivKeyHex := hex.EncodeToString(crypto.FromECDSA(chatPrivKey))
|
||||
|
||||
encryptionPrivKey, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
encryptionPrivKeyHex := hex.EncodeToString(crypto.FromECDSA(encryptionPrivKey))
|
||||
|
||||
err = s.Backend.InjectChatAccount(chatPrivKeyHex, encryptionPrivKeyHex)
|
||||
loginParams, err = buildLoginParams(accountInfo2.WalletAddress, accountInfo2.ChatAddress, utils.TestConfig.Account1.Password, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
selectedChatAccount, err := s.Backend.AccountManager().SelectedChatAccount()
|
||||
s.Require().NoError(err)
|
||||
s.Equal(chatPrivKey, selectedChatAccount.AccountKey.PrivateKey)
|
||||
s.NoError(s.Backend.SelectAccount(loginParams))
|
||||
}
|
||||
|
||||
func (s *AccountsTestSuite) TestSelectedAccountOnRestart() {
|
||||
|
@ -232,7 +231,10 @@ func (s *AccountsTestSuite) TestSelectedAccountOnRestart() {
|
|||
s.Nil(selectedChatAccount)
|
||||
|
||||
// select account
|
||||
err = s.Backend.SelectAccount(buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, "wrongPassword", nil))
|
||||
loginParams, err := buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, "wrongPassword", nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.Backend.SelectAccount(loginParams)
|
||||
expectedErr := errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given password")
|
||||
s.EqualError(expectedErr, err.Error())
|
||||
|
||||
|
@ -240,7 +242,11 @@ func (s *AccountsTestSuite) TestSelectedAccountOnRestart() {
|
|||
types.HexToAddress("0x00000000000000000000000000000000000001"),
|
||||
types.HexToAddress("0x00000000000000000000000000000000000002"),
|
||||
}
|
||||
s.NoError(s.Backend.SelectAccount(buildLoginParams(accountInfo2.WalletAddress, accountInfo2.ChatAddress, utils.TestConfig.Account1.Password, watchAddresses)))
|
||||
|
||||
loginParams, err = buildLoginParams(accountInfo2.WalletAddress, accountInfo2.ChatAddress, utils.TestConfig.Account1.Password, watchAddresses)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.NoError(s.Backend.SelectAccount(loginParams))
|
||||
|
||||
// stop node (and all of its sub-protocols)
|
||||
nodeConfig := s.Backend.StatusNode().Config()
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/account/generator"
|
||||
"github.com/status-im/status-go/eth-node/crypto"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/multiaccounts"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
|
@ -23,12 +25,21 @@ import (
|
|||
|
||||
type initFunc func([]byte, *transactions.SendTxArgs)
|
||||
|
||||
func buildLoginParams(mainAccountAddress, chatAddress, password string) account.LoginParams {
|
||||
return account.LoginParams{
|
||||
ChatAddress: types.HexToAddress(chatAddress),
|
||||
Password: password,
|
||||
MainAccount: types.HexToAddress(mainAccountAddress),
|
||||
func buildLoginParams(mainAccountAddress, chatAddress, password string) (account.LoginParams, error) {
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
return account.LoginParams{}, err
|
||||
}
|
||||
|
||||
acc := generator.NewAccount(privateKey, nil)
|
||||
iai := acc.ToIdentifiedAccountInfo("")
|
||||
|
||||
return account.LoginParams{
|
||||
ChatAddress: types.HexToAddress(chatAddress),
|
||||
Password: password,
|
||||
MainAccount: types.HexToAddress(mainAccountAddress),
|
||||
MultiAccount: iai.ToMultiAccount(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestTransactionsTestSuite(t *testing.T) {
|
||||
|
@ -85,11 +96,14 @@ func (s *TransactionsTestSuite) TestCallUpstreamPrivateRPCSendTransaction() {
|
|||
func (s *TransactionsTestSuite) sendTransactionUsingRPCClient(
|
||||
callRPCFn func(string) (string, error),
|
||||
) {
|
||||
err := s.Backend.SelectAccount(buildLoginParams(
|
||||
loginParams, err := buildLoginParams(
|
||||
utils.TestConfig.Account1.WalletAddress,
|
||||
utils.TestConfig.Account1.ChatAddress,
|
||||
utils.TestConfig.Account1.Password,
|
||||
))
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.Backend.SelectAccount(loginParams)
|
||||
s.NoError(err)
|
||||
|
||||
result, err := callRPCFn(`{
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
package whisper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/status-im/status-go/node"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/t/e2e"
|
||||
"github.com/status-im/status-go/t/utils"
|
||||
)
|
||||
|
||||
func TestMailServiceSuite(t *testing.T) {
|
||||
utils.Init()
|
||||
suite.Run(t, new(MailServiceSuite))
|
||||
}
|
||||
|
||||
type MailServiceSuite struct {
|
||||
e2e.StatusNodeTestSuite
|
||||
}
|
||||
|
||||
func (s *MailServiceSuite) SetupTest() {
|
||||
s.StatusNode = node.New()
|
||||
}
|
||||
|
||||
// TestShhextRequestMessagesRPCMethodAvailability tests if `shhext_requestMessages` is available
|
||||
// through inproc and HTTP interfaces.
|
||||
func (s *MailServiceSuite) TestShhextRequestMessagesRPCMethodAvailability() {
|
||||
r := s.Require()
|
||||
|
||||
s.StartTestNode(func(config *params.NodeConfig) {
|
||||
config.HTTPEnabled = true
|
||||
config.AddAPIModule("shhext")
|
||||
})
|
||||
defer s.StopTestNode()
|
||||
|
||||
client := s.StatusNode.RPCPrivateClient()
|
||||
r.NotNil(client)
|
||||
|
||||
// This error means that the method is available through inproc communication
|
||||
// as the validation of params occurred.
|
||||
err := client.Call(nil, "shhext_requestMessages", map[string]interface{}{
|
||||
"mailServerPeer": "xxx",
|
||||
})
|
||||
r.EqualError(err, `invalid mailServerPeer value: invalid URL scheme, want "enode"`)
|
||||
|
||||
// Do the same but using HTTP interface.
|
||||
req, err := http.NewRequest("POST", "http://localhost:8645", bytes.NewBuffer([]byte(`{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "shhext_requestMessages",
|
||||
"params": [{}]
|
||||
}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.NoError(err)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
r.NoError(err)
|
||||
defer func() {
|
||||
err := resp.Body.Close()
|
||||
r.NoError(err)
|
||||
}()
|
||||
r.Equal(200, resp.StatusCode)
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
r.NoError(err)
|
||||
r.Contains(string(data), `invalid mailServerPeer value`)
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package whisper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/status-im/status-go/node"
|
||||
"github.com/status-im/status-go/t/utils"
|
||||
)
|
||||
|
||||
func TestWhisperExtensionSuite(t *testing.T) {
|
||||
suite.Run(t, new(WhisperExtensionSuite))
|
||||
}
|
||||
|
||||
type WhisperExtensionSuite struct {
|
||||
suite.Suite
|
||||
|
||||
nodes []*node.StatusNode
|
||||
}
|
||||
|
||||
func (s *WhisperExtensionSuite) SetupTest() {
|
||||
s.nodes = make([]*node.StatusNode, 2)
|
||||
for i := range s.nodes {
|
||||
dir, err := ioutil.TempDir("", "test-shhext-")
|
||||
s.NoError(err)
|
||||
// network id is irrelevant
|
||||
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, nil))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WhisperExtensionSuite) TearDown() {
|
||||
for _, n := range s.nodes {
|
||||
cfg := n.Config()
|
||||
s.NotNil(cfg)
|
||||
s.NoError(n.Stop())
|
||||
s.NoError(os.Remove(cfg.DataDir))
|
||||
}
|
||||
}
|
|
@ -1,976 +0,0 @@
|
|||
package whisper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/status-im/status-go/services/shhext"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
|
||||
"github.com/status-im/status-go/api"
|
||||
"github.com/status-im/status-go/mailserver"
|
||||
"github.com/status-im/status-go/params"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/t/helpers"
|
||||
"github.com/status-im/status-go/t/utils"
|
||||
"github.com/status-im/status-go/whisper/v6"
|
||||
)
|
||||
|
||||
const mailboxPassword = "status-offline-inbox"
|
||||
|
||||
type WhisperMailboxSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestWhisperMailboxTestSuite(t *testing.T) {
|
||||
utils.Init()
|
||||
suite.Run(t, new(WhisperMailboxSuite))
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) TestRequestMessageFromMailboxAsync() {
|
||||
var err error
|
||||
// Start mailbox and status node.
|
||||
mailboxBackend, stop := s.startMailboxBackend("")
|
||||
defer stop()
|
||||
s.Require().True(mailboxBackend.IsNodeRunning())
|
||||
mailboxNode := mailboxBackend.StatusNode().GethNode()
|
||||
mailboxEnode := mailboxNode.Server().NodeInfo().Enode
|
||||
|
||||
sender, stop := s.startBackend("sender")
|
||||
defer stop()
|
||||
s.Require().True(sender.IsNodeRunning())
|
||||
node := sender.StatusNode().GethNode()
|
||||
|
||||
s.Require().NotEqual(mailboxEnode, node.Server().NodeInfo().Enode)
|
||||
|
||||
errCh := helpers.WaitForPeerAsync(sender.StatusNode().Server(), mailboxEnode, p2p.PeerEventTypeAdd, time.Second)
|
||||
s.Require().NoError(sender.StatusNode().AddPeer(mailboxEnode))
|
||||
s.Require().NoError(<-errCh)
|
||||
|
||||
senderWhisperService, err := sender.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// This sleep still is required because there is still a race condition
|
||||
// when adding a peer to p2p.Server and to Whisper service.
|
||||
// It may happen that the event about the added peer
|
||||
// was emitted by p2p.Server but Whisper service is not aware of it yet.
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// Mark mailbox node trusted.
|
||||
err = senderWhisperService.AllowP2PMessagesFromPeer(mailboxNode.Server().Self().ID().Bytes())
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Generate mailbox symkey.
|
||||
password := mailboxPassword
|
||||
MailServerKeyID, err := senderWhisperService.AddSymKeyFromPassword(password)
|
||||
s.Require().NoError(err)
|
||||
|
||||
rpcClient := sender.StatusNode().RPCPrivateClient()
|
||||
s.Require().NotNil(rpcClient)
|
||||
|
||||
mailboxWhisperService, err := mailboxBackend.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(mailboxWhisperService)
|
||||
|
||||
// watch envelopes to be archived on mailserver
|
||||
envelopeArchivedWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub := mailboxWhisperService.SubscribeEnvelopeEvents(envelopeArchivedWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// watch envelopes to be available for filters in the client
|
||||
envelopeAvailableWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub = senderWhisperService.SubscribeEnvelopeEvents(envelopeAvailableWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// watch mailserver responses in the client
|
||||
mailServerResponseWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub = senderWhisperService.SubscribeEnvelopeEvents(mailServerResponseWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Create topic.
|
||||
topic := whisper.BytesToTopic([]byte("topic name"))
|
||||
|
||||
// Add key pair to whisper.
|
||||
keyID, err := senderWhisperService.NewKeyPair()
|
||||
s.Require().NoError(err)
|
||||
key, err := senderWhisperService.GetPrivateKey(keyID)
|
||||
s.Require().NoError(err)
|
||||
pubkey := hexutil.Bytes(crypto.FromECDSAPub(&key.PublicKey))
|
||||
|
||||
// Create message filter.
|
||||
messageFilterID := s.createPrivateChatMessageFilter(rpcClient, keyID, topic.String())
|
||||
|
||||
// There are no messages at filter.
|
||||
messages := s.getMessagesByMessageFilterID(rpcClient, messageFilterID)
|
||||
s.Require().Empty(messages)
|
||||
|
||||
// Post message matching with filter (key and topic).
|
||||
messageHash := s.postMessageToPrivate(rpcClient, pubkey.String(), topic.String(), hexutil.Encode([]byte("Hello world!")))
|
||||
|
||||
// Get message to make sure that it will come from the mailbox later.
|
||||
s.waitForEnvelopeEvents(envelopeAvailableWatcher, []string{messageHash}, whisper.EventEnvelopeAvailable)
|
||||
messages = s.getMessagesByMessageFilterID(rpcClient, messageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
|
||||
// Act.
|
||||
|
||||
// wait for mailserver to archive all the envelopes
|
||||
s.waitForEnvelopeEvents(envelopeArchivedWatcher, []string{messageHash}, whisper.EventMailServerEnvelopeArchived)
|
||||
|
||||
// Request messages (including the previous one, expired) from mailbox.
|
||||
requestID := s.requestHistoricMessagesFromLast12Hours(senderWhisperService, rpcClient, mailboxNode.Server().Self().URLv4(), MailServerKeyID, topic.String(), 0, "")
|
||||
|
||||
// wait for mail server response
|
||||
resp := s.waitForMailServerResponse(mailServerResponseWatcher, requestID)
|
||||
s.Require().Equal(messageHash, resp.LastEnvelopeHash.String())
|
||||
s.Require().Empty(resp.Cursor)
|
||||
|
||||
// wait for last envelope sent by the mailserver to be available for filters
|
||||
s.waitForEnvelopeEvents(envelopeAvailableWatcher, []string{resp.LastEnvelopeHash.String()}, whisper.EventEnvelopeAvailable)
|
||||
|
||||
// And we receive message, it comes from mailbox.
|
||||
messages = s.getMessagesByMessageFilterID(rpcClient, messageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
|
||||
// Check that there are no messages.
|
||||
messages = s.getMessagesByMessageFilterID(rpcClient, messageFilterID)
|
||||
s.Require().Empty(messages)
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) TestRequestMessagesInGroupChat() {
|
||||
var err error
|
||||
|
||||
// Start mailbox, alice, bob, charlie node.
|
||||
mailboxBackend, stop := s.startMailboxBackend("")
|
||||
defer stop()
|
||||
|
||||
aliceBackend, stop := s.startBackend("alice")
|
||||
defer stop()
|
||||
|
||||
bobBackend, stop := s.startBackend("bob")
|
||||
defer stop()
|
||||
|
||||
charlieBackend, stop := s.startBackend("charlie")
|
||||
defer stop()
|
||||
|
||||
// Add mailbox to static peers.
|
||||
s.Require().True(mailboxBackend.IsNodeRunning())
|
||||
mailboxNode := mailboxBackend.StatusNode().GethNode()
|
||||
mailboxEnode := mailboxNode.Server().NodeInfo().Enode
|
||||
|
||||
aliceErrCh := helpers.WaitForPeerAsync(aliceBackend.StatusNode().Server(), mailboxEnode, p2p.PeerEventTypeAdd, 5*time.Second)
|
||||
bobErrCh := helpers.WaitForPeerAsync(bobBackend.StatusNode().Server(), mailboxEnode, p2p.PeerEventTypeAdd, 5*time.Second)
|
||||
charlieErrCh := helpers.WaitForPeerAsync(charlieBackend.StatusNode().Server(), mailboxEnode, p2p.PeerEventTypeAdd, 5*time.Second)
|
||||
|
||||
s.Require().NoError(aliceBackend.StatusNode().AddPeer(mailboxEnode))
|
||||
s.Require().NoError(bobBackend.StatusNode().AddPeer(mailboxEnode))
|
||||
s.Require().NoError(charlieBackend.StatusNode().AddPeer(mailboxEnode))
|
||||
|
||||
s.Require().NoError(<-aliceErrCh)
|
||||
s.Require().NoError(<-bobErrCh)
|
||||
s.Require().NoError(<-charlieErrCh)
|
||||
|
||||
// Get whisper service.
|
||||
mailboxWhisperService, err := mailboxBackend.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
aliceWhisperService, err := aliceBackend.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
bobWhisperService, err := bobBackend.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
charlieWhisperService, err := charlieBackend.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
// Get rpc client.
|
||||
aliceRPCClient := aliceBackend.StatusNode().RPCPrivateClient()
|
||||
bobRPCClient := bobBackend.StatusNode().RPCPrivateClient()
|
||||
charlieRPCClient := charlieBackend.StatusNode().RPCPrivateClient()
|
||||
|
||||
// watchers
|
||||
envelopeArchivedWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub := mailboxWhisperService.SubscribeEnvelopeEvents(envelopeArchivedWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
bobEnvelopeAvailableWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub = bobWhisperService.SubscribeEnvelopeEvents(bobEnvelopeAvailableWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
charlieEnvelopeAvailableWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub = charlieWhisperService.SubscribeEnvelopeEvents(charlieEnvelopeAvailableWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Bob and charlie add the mailserver key.
|
||||
password := mailboxPassword
|
||||
bobMailServerKeyID, err := bobWhisperService.AddSymKeyFromPassword(password)
|
||||
s.Require().NoError(err)
|
||||
charlieMailServerKeyID, err := charlieWhisperService.AddSymKeyFromPassword(password)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Generate a group chat symkey and topic.
|
||||
groupChatKeyID, err := aliceWhisperService.GenerateSymKey()
|
||||
s.Require().NoError(err)
|
||||
groupChatKey, err := aliceWhisperService.GetSymKey(groupChatKeyID)
|
||||
s.Require().NoError(err)
|
||||
// Generate a group chat topic.
|
||||
groupChatTopic := whisper.BytesToTopic([]byte("groupChatTopic"))
|
||||
// sender must be subscribed to message topic it sends
|
||||
s.NotNil(s.createGroupChatMessageFilter(aliceRPCClient, groupChatKeyID, groupChatTopic.String()))
|
||||
groupChatPayload := newGroupChatParams(groupChatKey, groupChatTopic)
|
||||
payloadStr, err := groupChatPayload.Encode()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Add Bob and Charlie's key pairs to receive the symmetric key for the group chat from Alice.
|
||||
bobKeyID, err := bobWhisperService.NewKeyPair()
|
||||
s.Require().NoError(err)
|
||||
bobKey, err := bobWhisperService.GetPrivateKey(bobKeyID)
|
||||
s.Require().NoError(err)
|
||||
bobPubkey := hexutil.Bytes(crypto.FromECDSAPub(&bobKey.PublicKey))
|
||||
bobAliceKeySendTopic := whisper.BytesToTopic([]byte("bobAliceKeySendTopic "))
|
||||
|
||||
charlieKeyID, err := charlieWhisperService.NewKeyPair()
|
||||
s.Require().NoError(err)
|
||||
charlieKey, err := charlieWhisperService.GetPrivateKey(charlieKeyID)
|
||||
s.Require().NoError(err)
|
||||
charliePubkey := hexutil.Bytes(crypto.FromECDSAPub(&charlieKey.PublicKey))
|
||||
charlieAliceKeySendTopic := whisper.BytesToTopic([]byte("charlieAliceKeySendTopic "))
|
||||
|
||||
// Alice must add peers topics into her own bloom filter.
|
||||
aliceKeyID, err := aliceWhisperService.NewKeyPair()
|
||||
s.Require().NoError(err)
|
||||
s.createPrivateChatMessageFilter(aliceRPCClient, aliceKeyID, bobAliceKeySendTopic.String())
|
||||
s.createPrivateChatMessageFilter(aliceRPCClient, aliceKeyID, charlieAliceKeySendTopic.String())
|
||||
|
||||
// Bob and charlie create message filter.
|
||||
bobMessageFilterID := s.createPrivateChatMessageFilter(bobRPCClient, bobKeyID, bobAliceKeySendTopic.String())
|
||||
charlieMessageFilterID := s.createPrivateChatMessageFilter(charlieRPCClient, charlieKeyID, charlieAliceKeySendTopic.String())
|
||||
|
||||
// Alice send message with symkey and topic to Bob and Charlie.
|
||||
aliceToBobMessageHash := s.postMessageToPrivate(aliceRPCClient, bobPubkey.String(), bobAliceKeySendTopic.String(), payloadStr)
|
||||
aliceToCharlieMessageHash := s.postMessageToPrivate(aliceRPCClient, charliePubkey.String(), charlieAliceKeySendTopic.String(), payloadStr)
|
||||
|
||||
// Bob receive group chat data and add it to his node.
|
||||
// Bob get group chat details.
|
||||
s.waitForEnvelopeEvents(bobEnvelopeAvailableWatcher, []string{aliceToBobMessageHash}, whisper.EventEnvelopeAvailable)
|
||||
messages := s.getMessagesByMessageFilterID(bobRPCClient, bobMessageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
bobGroupChatData := groupChatParams{}
|
||||
err = bobGroupChatData.Decode(messages[0]["payload"].(string))
|
||||
s.Require().NoError(err)
|
||||
s.EqualValues(groupChatPayload, bobGroupChatData)
|
||||
|
||||
// Bob add symkey to his node.
|
||||
bobGroupChatSymkeyID := s.addSymKey(bobRPCClient, bobGroupChatData.Key)
|
||||
s.Require().NotEmpty(bobGroupChatSymkeyID)
|
||||
|
||||
// Bob create message filter to node by group chat topic.
|
||||
bobGroupChatMessageFilterID := s.createGroupChatMessageFilter(bobRPCClient, bobGroupChatSymkeyID, bobGroupChatData.Topic)
|
||||
|
||||
// Charlie receive group chat data and add it to his node.
|
||||
// Charlie get group chat details.
|
||||
s.waitForEnvelopeEvents(charlieEnvelopeAvailableWatcher, []string{aliceToCharlieMessageHash}, whisper.EventEnvelopeAvailable)
|
||||
messages = s.getMessagesByMessageFilterID(charlieRPCClient, charlieMessageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
charlieGroupChatData := groupChatParams{}
|
||||
err = charlieGroupChatData.Decode(messages[0]["payload"].(string))
|
||||
s.Require().NoError(err)
|
||||
s.EqualValues(groupChatPayload, charlieGroupChatData)
|
||||
|
||||
// Charlie add symkey to his node.
|
||||
charlieGroupChatSymkeyID := s.addSymKey(charlieRPCClient, charlieGroupChatData.Key)
|
||||
s.Require().NotEmpty(charlieGroupChatSymkeyID)
|
||||
|
||||
// Charlie create message filter to node by group chat topic.
|
||||
charlieGroupChatMessageFilterID := s.createGroupChatMessageFilter(charlieRPCClient, charlieGroupChatSymkeyID, charlieGroupChatData.Topic)
|
||||
|
||||
// Alice send message to group chat.
|
||||
helloWorldMessage := hexutil.Encode([]byte("Hello world!"))
|
||||
groupChatMessageHash := s.postMessageToGroup(aliceRPCClient, groupChatKeyID, groupChatTopic.String(), helloWorldMessage)
|
||||
|
||||
// Bob receive group chat message.
|
||||
s.waitForEnvelopeEvents(bobEnvelopeAvailableWatcher, []string{groupChatMessageHash}, whisper.EventEnvelopeAvailable)
|
||||
messages = s.getMessagesByMessageFilterID(bobRPCClient, bobGroupChatMessageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
s.Require().Equal(helloWorldMessage, messages[0]["payload"].(string))
|
||||
|
||||
// Charlie receive group chat message.
|
||||
s.waitForEnvelopeEvents(charlieEnvelopeAvailableWatcher, []string{groupChatMessageHash}, whisper.EventEnvelopeAvailable)
|
||||
messages = s.getMessagesByMessageFilterID(charlieRPCClient, charlieGroupChatMessageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
s.Require().Equal(helloWorldMessage, messages[0]["payload"].(string))
|
||||
|
||||
// Check that we don't receive messages each one time.
|
||||
messages = s.getMessagesByMessageFilterID(bobRPCClient, bobGroupChatMessageFilterID)
|
||||
s.Require().Empty(messages)
|
||||
messages = s.getMessagesByMessageFilterID(charlieRPCClient, charlieGroupChatMessageFilterID)
|
||||
s.Require().Empty(messages)
|
||||
|
||||
// be sure that message has been archived
|
||||
s.waitForEnvelopeEvents(envelopeArchivedWatcher, []string{groupChatMessageHash}, whisper.EventMailServerEnvelopeArchived)
|
||||
|
||||
// Request each one messages from mailbox using enode.
|
||||
s.requestHistoricMessagesFromLast12Hours(bobWhisperService, bobRPCClient, mailboxEnode, bobMailServerKeyID, groupChatTopic.String(), 0, "")
|
||||
s.requestHistoricMessagesFromLast12Hours(charlieWhisperService, charlieRPCClient, mailboxEnode, charlieMailServerKeyID, groupChatTopic.String(), 0, "")
|
||||
|
||||
// Bob receive p2p message from group chat filter.
|
||||
s.waitForEnvelopeEvents(bobEnvelopeAvailableWatcher, []string{groupChatMessageHash}, whisper.EventEnvelopeAvailable)
|
||||
messages = s.getMessagesByMessageFilterID(bobRPCClient, bobGroupChatMessageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
s.Require().Equal(helloWorldMessage, messages[0]["payload"].(string))
|
||||
|
||||
// Charlie receive p2p message from group chat filter.
|
||||
s.waitForEnvelopeEvents(charlieEnvelopeAvailableWatcher, []string{groupChatMessageHash}, whisper.EventEnvelopeAvailable)
|
||||
messages = s.getMessagesByMessageFilterID(charlieRPCClient, charlieGroupChatMessageFilterID)
|
||||
s.Require().Equal(1, len(messages))
|
||||
s.Require().Equal(helloWorldMessage, messages[0]["payload"].(string))
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) TestRequestMessagesWithPagination() {
|
||||
// Start mailbox
|
||||
mailbox, stop := s.startMailboxBackend("")
|
||||
defer stop()
|
||||
s.Require().True(mailbox.IsNodeRunning())
|
||||
mailboxEnode := mailbox.StatusNode().GethNode().Server().NodeInfo().Enode
|
||||
|
||||
// Start client
|
||||
client, stop := s.startBackend("client")
|
||||
defer stop()
|
||||
s.Require().True(client.IsNodeRunning())
|
||||
clientRPCClient := client.StatusNode().RPCPrivateClient()
|
||||
|
||||
// Add mailbox to client's peers
|
||||
errCh := helpers.WaitForPeerAsync(client.StatusNode().Server(), mailboxEnode, p2p.PeerEventTypeAdd, 5*time.Second)
|
||||
s.Require().NoError(client.StatusNode().AddPeer(mailboxEnode))
|
||||
s.Require().NoError(<-errCh)
|
||||
|
||||
// Whisper services
|
||||
mailboxWhisperService, err := mailbox.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
clientWhisperService, err := client.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
// mailserver sym key
|
||||
mailServerKeyID, err := clientWhisperService.AddSymKeyFromPassword(mailboxPassword)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// public chat
|
||||
var (
|
||||
keyID string
|
||||
topic whisper.TopicType
|
||||
filterID string
|
||||
)
|
||||
publicChatName := "test public chat"
|
||||
keyID, topic, filterID = s.joinPublicChat(clientWhisperService, clientRPCClient, publicChatName)
|
||||
|
||||
// envelopes to be sent
|
||||
envelopesCount := 5
|
||||
sentEnvelopesHashes := make([]string, 0)
|
||||
|
||||
// watch envelopes to be archived on mailserver
|
||||
envelopeArchivedWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub := mailboxWhisperService.SubscribeEnvelopeEvents(envelopeArchivedWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// watch envelopes to be available for filters in the client
|
||||
envelopeAvailableWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub = clientWhisperService.SubscribeEnvelopeEvents(envelopeAvailableWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// watch mailserver responses in the client
|
||||
mailServerResponseWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub = clientWhisperService.SubscribeEnvelopeEvents(mailServerResponseWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// send envelopes
|
||||
for i := 0; i < envelopesCount; i++ {
|
||||
hash := s.postMessageToGroup(clientRPCClient, keyID, topic.String(), "")
|
||||
sentEnvelopesHashes = append(sentEnvelopesHashes, hash)
|
||||
}
|
||||
|
||||
// get messages from filter before requesting them to mailserver
|
||||
lastEnvelopeHash := sentEnvelopesHashes[len(sentEnvelopesHashes)-1]
|
||||
s.waitForEnvelopeEvents(envelopeAvailableWatcher, []string{lastEnvelopeHash}, whisper.EventEnvelopeAvailable)
|
||||
messages := s.getMessagesByMessageFilterID(clientRPCClient, filterID)
|
||||
s.Equal(envelopesCount, len(messages))
|
||||
|
||||
messages = s.getMessagesByMessageFilterID(clientRPCClient, filterID)
|
||||
s.Equal(0, len(messages))
|
||||
|
||||
limit := 3
|
||||
|
||||
getMessages := func() []string {
|
||||
envelopes := s.getMessagesByMessageFilterID(clientRPCClient, filterID)
|
||||
hashes := make([]string, 0)
|
||||
for _, e := range envelopes {
|
||||
hashes = append(hashes, e["hash"].(string))
|
||||
}
|
||||
|
||||
return hashes
|
||||
}
|
||||
|
||||
requestMessages := func(cursor string) common.Hash {
|
||||
return s.requestHistoricMessagesFromLast12Hours(clientWhisperService, clientRPCClient, mailboxEnode, mailServerKeyID, topic.String(), limit, cursor)
|
||||
}
|
||||
|
||||
// wait for mailserver to archive all the envelopes
|
||||
s.waitForEnvelopeEvents(envelopeArchivedWatcher, sentEnvelopesHashes, whisper.EventMailServerEnvelopeArchived)
|
||||
|
||||
// first page
|
||||
// send request
|
||||
requestID := requestMessages("")
|
||||
// wait for mail server response
|
||||
resp := s.waitForMailServerResponse(mailServerResponseWatcher, requestID)
|
||||
s.NotEmpty(resp.LastEnvelopeHash)
|
||||
s.NotEmpty(resp.Cursor)
|
||||
// wait for last envelope sent by the mailserver to be available for filters
|
||||
s.waitForEnvelopeEvents(envelopeAvailableWatcher, []string{resp.LastEnvelopeHash.String()}, whisper.EventEnvelopeAvailable)
|
||||
// get messages
|
||||
firstPageHashes := getMessages()
|
||||
s.Equal(3, len(firstPageHashes))
|
||||
|
||||
// second page
|
||||
// send request
|
||||
requestID = requestMessages(fmt.Sprintf("%x", resp.Cursor))
|
||||
// wait for mail server response
|
||||
resp = s.waitForMailServerResponse(mailServerResponseWatcher, requestID)
|
||||
s.NotEmpty(resp.LastEnvelopeHash)
|
||||
// all messages have been sent, no more pages available
|
||||
s.Empty(resp.Cursor)
|
||||
// wait for last envelope sent by the mailserver to be available for filters
|
||||
s.waitForEnvelopeEvents(envelopeAvailableWatcher, []string{resp.LastEnvelopeHash.String()}, whisper.EventEnvelopeAvailable)
|
||||
// get messages
|
||||
secondPageHashes := getMessages()
|
||||
s.Equal(2, len(secondPageHashes))
|
||||
|
||||
allReceivedHashes := append(firstPageHashes, secondPageHashes...)
|
||||
s.Equal(envelopesCount, len(allReceivedHashes))
|
||||
|
||||
// check that all the envelopes have been received
|
||||
sort.Strings(sentEnvelopesHashes)
|
||||
sort.Strings(allReceivedHashes)
|
||||
s.Equal(sentEnvelopesHashes, allReceivedHashes)
|
||||
}
|
||||
|
||||
// TestSyncBetweenTwoMailServers tests syncing state between two mail servers
|
||||
// using `shhext_requestMessages`.
|
||||
func (s *WhisperMailboxSuite) TestSyncBetweenTwoMailServers() {
|
||||
var (
|
||||
chatName = "some-public-chat"
|
||||
)
|
||||
|
||||
// Start mailbox but with mail server disabled.
|
||||
// MailServer will be registered below manually.
|
||||
mailbox, stop := s.startMailboxBackendWithCallback("", func(c *params.NodeConfig) {
|
||||
c.WhisperConfig.EnableMailServer = false
|
||||
})
|
||||
defer stop()
|
||||
s.Require().True(mailbox.IsNodeRunning())
|
||||
mailboxEnode := mailbox.StatusNode().GethNode().Server().NodeInfo().Enode
|
||||
|
||||
// Whisper services
|
||||
mailboxWhisperService, err := mailbox.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// symmetric key for public chats
|
||||
symKeyID, err := mailboxWhisperService.AddSymKeyFromPassword(chatName)
|
||||
s.Require().NoError(err)
|
||||
publicChatSymKey, err := mailboxWhisperService.GetSymKey(symKeyID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
var mailServer mailserver.WhisperMailServer
|
||||
err = mailServer.Init(mailboxWhisperService, &mailbox.StatusNode().Config().WhisperConfig)
|
||||
s.Require().NoError(err)
|
||||
mailboxWhisperService.RegisterMailServer(&mailServer)
|
||||
|
||||
// envelopes to archive
|
||||
envelopesCount := 5
|
||||
topic := s.createPublicChatTopic(chatName)
|
||||
for i := 0; i < envelopesCount; i++ {
|
||||
params := whisper.MessageParams{
|
||||
Topic: topic,
|
||||
WorkTime: 10,
|
||||
PoW: 2.0,
|
||||
Payload: []byte("some-payload"),
|
||||
KeySym: publicChatSymKey,
|
||||
}
|
||||
sentMessage, err := whisper.NewSentMessage(¶ms)
|
||||
s.Require().NoError(err)
|
||||
envelope, err := sentMessage.Wrap(¶ms, time.Now())
|
||||
s.Require().NoError(err)
|
||||
|
||||
mailServer.Archive(envelope)
|
||||
}
|
||||
|
||||
// Start a second mail server which would like to sync the state.
|
||||
emptyMailbox, stopEmptyMailbox := s.startMailboxBackend("empty")
|
||||
defer stopEmptyMailbox()
|
||||
s.Require().True(emptyMailbox.IsNodeRunning())
|
||||
|
||||
emptyMailboxEnode := emptyMailbox.StatusNode().Server().Self().URLv4()
|
||||
|
||||
errCh := helpers.WaitForPeerAsync(emptyMailbox.StatusNode().Server(), mailboxEnode, p2p.PeerEventTypeAdd, 5*time.Second)
|
||||
s.Require().NoError(emptyMailbox.StatusNode().AddPeer(mailboxEnode))
|
||||
s.Require().NoError(<-errCh)
|
||||
|
||||
emptyMailboxWhisperService, err := emptyMailbox.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = utils.Eventually(func() error {
|
||||
return emptyMailboxWhisperService.AllowP2PMessagesFromPeer(
|
||||
mailbox.StatusNode().Server().Self().ID().Bytes(),
|
||||
)
|
||||
}, time.Second*5, time.Millisecond*100)
|
||||
s.Require().NoError(err)
|
||||
|
||||
emptyMailboxRPCClient := emptyMailbox.StatusNode().RPCPrivateClient()
|
||||
s.Require().NotNil(emptyMailboxRPCClient)
|
||||
|
||||
// Ask to sync the first batch of messages.
|
||||
// We artificially set Limit to 1 in order to test pagination.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
var syncMessagesResponse shhext.SyncMessagesResponse
|
||||
err = emptyMailboxRPCClient.CallContext(
|
||||
ctx,
|
||||
&syncMessagesResponse,
|
||||
"shhext_syncMessages",
|
||||
shhext.SyncMessagesRequest{
|
||||
MailServerPeer: mailbox.StatusNode().Server().Self().URLv4(),
|
||||
From: uint32(time.Now().Add(-time.Hour).Unix()),
|
||||
To: uint32(time.Now().Unix()),
|
||||
Limit: 1,
|
||||
},
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotEmpty(syncMessagesResponse.Cursor)
|
||||
s.Require().Empty(syncMessagesResponse.Error)
|
||||
|
||||
// Ask to sync the rest of messages.
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
err = emptyMailboxRPCClient.CallContext(
|
||||
ctx,
|
||||
&syncMessagesResponse,
|
||||
"shhext_syncMessages",
|
||||
shhext.SyncMessagesRequest{
|
||||
MailServerPeer: mailbox.StatusNode().Server().Self().URLv4(),
|
||||
From: uint32(time.Now().Add(-time.Hour).Unix()),
|
||||
To: uint32(time.Now().Unix()),
|
||||
Limit: 10,
|
||||
Cursor: syncMessagesResponse.Cursor,
|
||||
},
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Empty(syncMessagesResponse.Cursor)
|
||||
s.Require().Empty(syncMessagesResponse.Error)
|
||||
|
||||
// create and start a client
|
||||
client, stop := s.startBackend("client")
|
||||
defer stop()
|
||||
s.Require().True(client.IsNodeRunning())
|
||||
clientWhisperService, err := client.StatusNode().WhisperService()
|
||||
s.Require().NoError(err)
|
||||
clientRPCClient := client.StatusNode().RPCPrivateClient()
|
||||
|
||||
// create filter for messages
|
||||
messagesKeyID, err := clientWhisperService.AddSymKeyFromPassword(chatName)
|
||||
s.Require().NoError(err)
|
||||
messageFilterID := s.createGroupChatMessageFilter(clientRPCClient, messagesKeyID, topic.String())
|
||||
|
||||
// add mailbox password
|
||||
mailServerKeyID, err := clientWhisperService.AddSymKeyFromPassword(mailboxPassword)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// add the second mail server as a peer
|
||||
errCh = helpers.WaitForPeerAsync(
|
||||
client.StatusNode().Server(),
|
||||
emptyMailboxEnode,
|
||||
p2p.PeerEventTypeAdd,
|
||||
5*time.Second)
|
||||
s.Require().NoError(client.StatusNode().AddPeer(emptyMailboxEnode))
|
||||
s.Require().NoError(<-errCh)
|
||||
|
||||
// request for messages
|
||||
mailboxResponseWatcher := make(chan whisper.EnvelopeEvent, 1024)
|
||||
sub := clientWhisperService.SubscribeEnvelopeEvents(mailboxResponseWatcher)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
requestID := s.requestHistoricMessagesFromLast12Hours(
|
||||
clientWhisperService,
|
||||
clientRPCClient,
|
||||
emptyMailboxEnode,
|
||||
mailServerKeyID,
|
||||
topic.String(),
|
||||
0,
|
||||
"",
|
||||
)
|
||||
response := s.waitForMailServerResponse(mailboxResponseWatcher, requestID)
|
||||
s.Require().NotEqual(common.Hash{}.Bytes(), response.LastEnvelopeHash.Bytes())
|
||||
|
||||
// get messages
|
||||
messages := s.getMessagesByMessageFilterID(clientRPCClient, messageFilterID)
|
||||
// call once again because some messages can be delievered later
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
messages = append(
|
||||
messages,
|
||||
s.getMessagesByMessageFilterID(clientRPCClient, messageFilterID)...)
|
||||
|
||||
s.Require().Len(messages, envelopesCount)
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) waitForEnvelopeEvents(events chan whisper.EnvelopeEvent, hashes []string, event whisper.EventType) {
|
||||
check := make(map[string]struct{})
|
||||
for _, hash := range hashes {
|
||||
check[hash] = struct{}{}
|
||||
}
|
||||
|
||||
timeout := time.NewTimer(time.Second * 5)
|
||||
for {
|
||||
select {
|
||||
case e := <-events:
|
||||
if e.Event == event {
|
||||
delete(check, e.Hash.String())
|
||||
if len(check) == 0 {
|
||||
timeout.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-timeout.C:
|
||||
s.FailNow("timed out while waiting for event on envelopes", "event: %s", event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) waitForMailServerResponse(events chan whisper.EnvelopeEvent, requestID common.Hash) *whisper.MailServerResponse {
|
||||
timeout := time.NewTimer(time.Second * 5)
|
||||
for {
|
||||
select {
|
||||
case event := <-events:
|
||||
if event.Event == whisper.EventMailServerRequestCompleted && event.Hash == requestID {
|
||||
timeout.Stop()
|
||||
resp, ok := event.Data.(*whisper.MailServerResponse)
|
||||
if !ok {
|
||||
s.FailNow("mailserver response error", "expected whisper.MailServerResponse, got: %+v", resp)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
case <-timeout.C:
|
||||
s.FailNow("timed out while waiting for mailserver response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newGroupChatParams(symkey []byte, topic whisper.TopicType) groupChatParams {
|
||||
groupChatKeyStr := hexutil.Bytes(symkey).String()
|
||||
return groupChatParams{
|
||||
Key: groupChatKeyStr,
|
||||
Topic: topic.String(),
|
||||
}
|
||||
}
|
||||
|
||||
type groupChatParams struct {
|
||||
Key string
|
||||
Topic string
|
||||
}
|
||||
|
||||
func (d *groupChatParams) Decode(i string) error {
|
||||
b, err := hexutil.Decode(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, &d)
|
||||
}
|
||||
|
||||
func (d *groupChatParams) Encode() (string, error) {
|
||||
payload, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hexutil.Bytes(payload).String(), nil
|
||||
}
|
||||
|
||||
// Start status node.
|
||||
func (s *WhisperMailboxSuite) startBackend(name string) (*api.GethStatusBackend, func()) {
|
||||
datadir := filepath.Join(utils.RootDir, ".ethereumtest/mailbox", name)
|
||||
backend := api.NewGethStatusBackend()
|
||||
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
|
||||
|
||||
if addr, err := utils.GetRemoteURL(); err == nil {
|
||||
nodeConfig.UpstreamConfig.Enabled = true
|
||||
nodeConfig.UpstreamConfig.URL = addr
|
||||
}
|
||||
|
||||
s.Require().NoError(backend.StartNode(nodeConfig))
|
||||
s.Require().True(backend.IsNodeRunning())
|
||||
|
||||
return backend, func() {
|
||||
s.True(backend.IsNodeRunning())
|
||||
s.NoError(backend.StopNode())
|
||||
s.False(backend.IsNodeRunning())
|
||||
err = os.RemoveAll(datadir)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Start mailbox node.
|
||||
func (s *WhisperMailboxSuite) startMailboxBackend(name string) (*api.GethStatusBackend, func()) {
|
||||
return s.startMailboxBackendWithCallback(name, nil)
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) startMailboxBackendWithCallback(
|
||||
name string,
|
||||
callback func(*params.NodeConfig),
|
||||
) (*api.GethStatusBackend, func()) {
|
||||
if name == "" {
|
||||
name = "mailserver"
|
||||
}
|
||||
|
||||
mailboxConfig, err := utils.MakeTestNodeConfig(utils.GetNetworkID())
|
||||
s.Require().NoError(err)
|
||||
|
||||
mailboxBackend := api.NewGethStatusBackend()
|
||||
s.Require().NoError(mailboxBackend.AccountManager().InitKeystore(mailboxConfig.KeyStoreDir))
|
||||
datadir := filepath.Join(utils.RootDir, ".ethereumtest/mailbox", name)
|
||||
|
||||
mailboxConfig.LightEthConfig.Enabled = false
|
||||
mailboxConfig.WhisperConfig.Enabled = true
|
||||
mailboxConfig.KeyStoreDir = datadir
|
||||
mailboxConfig.WhisperConfig.EnableMailServer = true
|
||||
mailboxConfig.WhisperConfig.MailServerPassword = "status-offline-inbox"
|
||||
mailboxConfig.WhisperConfig.DataDir = filepath.Join(datadir, "data")
|
||||
mailboxConfig.DataDir = datadir
|
||||
|
||||
if callback != nil {
|
||||
callback(mailboxConfig)
|
||||
}
|
||||
|
||||
s.Require().False(mailboxBackend.IsNodeRunning())
|
||||
s.Require().NoError(mailboxBackend.StartNode(mailboxConfig))
|
||||
s.Require().True(mailboxBackend.IsNodeRunning())
|
||||
return mailboxBackend, func() {
|
||||
s.True(mailboxBackend.IsNodeRunning())
|
||||
s.NoError(mailboxBackend.StopNode())
|
||||
s.False(mailboxBackend.IsNodeRunning())
|
||||
err := os.RemoveAll(datadir)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createPrivateChatMessageFilter create message filter with asymmetric encryption.
|
||||
func (s *WhisperMailboxSuite) createPrivateChatMessageFilter(rpcCli *rpc.Client, privateKeyID string, topic string) string {
|
||||
resp := rpcCli.CallRaw(`{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "shh_newMessageFilter", "params": [
|
||||
{"privateKeyID": "` + privateKeyID + `", "topics": [ "` + topic + `"], "allowP2P":true}
|
||||
],
|
||||
"id": 1
|
||||
}`)
|
||||
|
||||
msgFilterResp := returnedIDResponse{}
|
||||
err := json.Unmarshal([]byte(resp), &msgFilterResp)
|
||||
messageFilterID := msgFilterResp.Result
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(msgFilterResp.Error)
|
||||
s.Require().NotEqual("", messageFilterID, resp)
|
||||
return messageFilterID
|
||||
}
|
||||
|
||||
// createGroupChatMessageFilter create message filter with symmetric encryption.
|
||||
func (s *WhisperMailboxSuite) createGroupChatMessageFilter(rpcCli *rpc.Client, symkeyID string, topic string) string {
|
||||
resp := rpcCli.CallRaw(`{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "shh_newMessageFilter", "params": [
|
||||
{"symKeyID": "` + symkeyID + `", "topics": ["` + topic + `"], "allowP2P": true}
|
||||
],
|
||||
"id": 1
|
||||
}`)
|
||||
|
||||
msgFilterResp := returnedIDResponse{}
|
||||
err := json.Unmarshal([]byte(resp), &msgFilterResp)
|
||||
messageFilterID := msgFilterResp.Result
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(msgFilterResp.Error)
|
||||
s.Require().NotEqual("", messageFilterID, resp)
|
||||
return messageFilterID
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) postMessageToPrivate(rpcCli *rpc.Client, recipientPubkey string, topic string, payload string) string {
|
||||
resp := rpcCli.CallRaw(`{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "shh_post",
|
||||
"params": [
|
||||
{
|
||||
"pubKey": "` + recipientPubkey + `",
|
||||
"topic": "` + topic + `",
|
||||
"payload": "` + payload + `",
|
||||
"powTarget": 0.001,
|
||||
"powTime": 2
|
||||
}
|
||||
],
|
||||
"id": 1}`)
|
||||
postResp := baseRPCResponse{}
|
||||
err := json.Unmarshal([]byte(resp), &postResp)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(postResp.Error)
|
||||
|
||||
return postResp.Result.(string)
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) postMessageToGroup(rpcCli *rpc.Client, groupChatKeyID string, topic string, payload string) string {
|
||||
resp := rpcCli.CallRaw(`{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "shh_post",
|
||||
"params": [
|
||||
{
|
||||
"symKeyID": "` + groupChatKeyID + `",
|
||||
"topic": "` + topic + `",
|
||||
"payload": "` + payload + `",
|
||||
"powTarget": 0.001,
|
||||
"powTime": 2
|
||||
}
|
||||
],
|
||||
"id": 1}`)
|
||||
postResp := baseRPCResponse{}
|
||||
err := json.Unmarshal([]byte(resp), &postResp)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(postResp.Error)
|
||||
|
||||
hash, ok := postResp.Result.(string)
|
||||
if !ok {
|
||||
s.FailNow("error decoding result", "expected string, got: %+v", postResp.Result)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(hash, "0x") {
|
||||
s.FailNow("hash format error", "expected hex string, got: %s", hash)
|
||||
}
|
||||
|
||||
return hash
|
||||
}
|
||||
|
||||
// getMessagesByMessageFilterID gets received messages by messageFilterID.
|
||||
func (s *WhisperMailboxSuite) getMessagesByMessageFilterID(rpcCli *rpc.Client, messageFilterID string) []map[string]interface{} {
|
||||
resp := rpcCli.CallRaw(`{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "shh_getFilterMessages",
|
||||
"params": ["` + messageFilterID + `"],
|
||||
"id": 1}`)
|
||||
messages := getFilterMessagesResponse{}
|
||||
err := json.Unmarshal([]byte(resp), &messages)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(messages.Error)
|
||||
return messages.Result
|
||||
}
|
||||
|
||||
// addSymKey added symkey to node and return symkeyID.
|
||||
func (s *WhisperMailboxSuite) addSymKey(rpcCli *rpc.Client, symkey string) string {
|
||||
resp := rpcCli.CallRaw(`{"jsonrpc":"2.0","method":"shh_addSymKey",
|
||||
"params":["` + symkey + `"],
|
||||
"id":1}`)
|
||||
symkeyAddResp := returnedIDResponse{}
|
||||
err := json.Unmarshal([]byte(resp), &symkeyAddResp)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(symkeyAddResp.Error)
|
||||
symkeyID := symkeyAddResp.Result
|
||||
s.Require().NotEmpty(symkeyID)
|
||||
return symkeyID
|
||||
}
|
||||
|
||||
// requestHistoricMessagesFromLast12Hours asks a mailnode to resend messages from last 12 hours.
|
||||
func (s *WhisperMailboxSuite) requestHistoricMessagesFromLast12Hours(w *whisper.Whisper, rpcCli *rpc.Client, mailboxEnode, mailServerKeyID, topic string, limit int, cursor string) common.Hash {
|
||||
currentTime := w.GetCurrentTime()
|
||||
from := currentTime.Add(-12 * time.Hour)
|
||||
to := currentTime
|
||||
return s.requestHistoricMessages(w, rpcCli, mailboxEnode, mailServerKeyID, topic, from, to, limit, cursor)
|
||||
}
|
||||
|
||||
// requestHistoricMessages asks a mailnode to resend messages.
|
||||
func (s *WhisperMailboxSuite) requestHistoricMessages(w *whisper.Whisper, rpcCli *rpc.Client, mailboxEnode, mailServerKeyID, topic string, from, to time.Time, limit int, cursor string) common.Hash {
|
||||
resp := rpcCli.CallRaw(`{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "shhext_requestMessages",
|
||||
"params": [{
|
||||
"mailServerPeer":"` + mailboxEnode + `",
|
||||
"topic":"` + topic + `",
|
||||
"symKeyID":"` + mailServerKeyID + `",
|
||||
"from":` + strconv.FormatInt(from.Unix(), 10) + `,
|
||||
"to":` + strconv.FormatInt(to.Unix(), 10) + `,
|
||||
"limit": ` + fmt.Sprintf("%d", limit) + `,
|
||||
"cursor": "` + cursor + `"
|
||||
}]
|
||||
}`)
|
||||
reqMessagesResp := baseRPCResponse{}
|
||||
err := json.Unmarshal([]byte(resp), &reqMessagesResp)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(reqMessagesResp.Error)
|
||||
|
||||
switch hash := reqMessagesResp.Result.(type) {
|
||||
case string:
|
||||
s.Require().True(strings.HasPrefix(hash, "0x"))
|
||||
b, err := hex.DecodeString(hash[2:])
|
||||
s.Require().NoError(err)
|
||||
return common.BytesToHash(b)
|
||||
default:
|
||||
s.Failf("failed reading shh_newMessageFilter result", "expected a hash, got: %+v", reqMessagesResp.Result)
|
||||
}
|
||||
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) createPublicChatTopic(name string) whisper.TopicType {
|
||||
h := sha3.NewLegacyKeccak256()
|
||||
_, err := h.Write([]byte(name))
|
||||
if err != nil {
|
||||
s.Fail("error generating topic", "failed gerating topic from chat name, %+v", err)
|
||||
}
|
||||
return whisper.BytesToTopic(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *WhisperMailboxSuite) joinPublicChat(w *whisper.Whisper, rpcClient *rpc.Client, name string) (string, whisper.TopicType, string) {
|
||||
keyID, err := w.AddSymKeyFromPassword(name)
|
||||
s.Require().NoError(err)
|
||||
|
||||
topic := s.createPublicChatTopic(name)
|
||||
|
||||
filterID := s.createGroupChatMessageFilter(rpcClient, keyID, topic.String())
|
||||
|
||||
return keyID, topic, filterID
|
||||
}
|
||||
|
||||
type getFilterMessagesResponse struct {
|
||||
Result []map[string]interface{}
|
||||
Error interface{}
|
||||
}
|
||||
|
||||
type returnedIDResponse struct {
|
||||
Result string
|
||||
Error interface{}
|
||||
}
|
||||
type baseRPCResponse struct {
|
||||
Result interface{}
|
||||
Error interface{}
|
||||
}
|
|
@ -1,275 +0,0 @@
|
|||
package whisper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/status-im/status-go/account"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
e2e "github.com/status-im/status-go/t/e2e"
|
||||
. "github.com/status-im/status-go/t/utils"
|
||||
"github.com/status-im/status-go/whisper/v6"
|
||||
)
|
||||
|
||||
func TestWhisperTestSuite(t *testing.T) {
|
||||
e2e.Init()
|
||||
suite.Run(t, new(WhisperTestSuite))
|
||||
}
|
||||
|
||||
type WhisperTestSuite struct {
|
||||
e2e.BackendTestSuite
|
||||
}
|
||||
|
||||
func buildLoginParams(mainAccountAddress, chatAddress, password string) account.LoginParams {
|
||||
return account.LoginParams{
|
||||
ChatAddress: types.HexToAddress(chatAddress),
|
||||
Password: password,
|
||||
MainAccount: types.HexToAddress(mainAccountAddress),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(adam): can anyone explain what this test is testing?
|
||||
// I don't see any race condition testing here.
|
||||
func (s *WhisperTestSuite) TestWhisperFilterRace() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
whisperService, err := s.Backend.StatusNode().WhisperService()
|
||||
s.NoError(err)
|
||||
|
||||
accountManager := s.Backend.AccountManager()
|
||||
s.NotNil(accountManager)
|
||||
|
||||
whisperAPI := whisper.NewPublicWhisperAPI(whisperService)
|
||||
|
||||
// account1
|
||||
_, accountKey1, err := accountManager.AddressToDecryptedAccount(TestConfig.Account1.ChatAddress, TestConfig.Account1.Password)
|
||||
s.NoError(err)
|
||||
accountKey1Byte := crypto.FromECDSAPub(&accountKey1.PrivateKey.PublicKey)
|
||||
|
||||
key1ID, err := whisperService.AddKeyPair(accountKey1.PrivateKey)
|
||||
s.NoError(err)
|
||||
ok := whisperAPI.HasKeyPair(context.Background(), key1ID)
|
||||
s.True(ok, "identity not injected")
|
||||
|
||||
// account2
|
||||
_, accountKey2, err := accountManager.AddressToDecryptedAccount(TestConfig.Account2.ChatAddress, TestConfig.Account2.Password)
|
||||
s.NoError(err)
|
||||
key2ID, err := whisperService.AddKeyPair(accountKey2.PrivateKey)
|
||||
s.NoError(err)
|
||||
ok = whisperAPI.HasKeyPair(context.Background(), key2ID)
|
||||
s.True(ok, "identity not injected")
|
||||
|
||||
// race filter addition
|
||||
filterAdded := make(chan struct{})
|
||||
allFiltersAdded := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
counter := 10
|
||||
for range filterAdded {
|
||||
counter--
|
||||
if counter <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
close(allFiltersAdded)
|
||||
}()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
// nolint: errcheck
|
||||
whisperAPI.NewMessageFilter(whisper.Criteria{
|
||||
Sig: accountKey1Byte,
|
||||
PrivateKeyID: key2ID,
|
||||
Topics: []whisper.TopicType{
|
||||
{0x4e, 0x03, 0x65, 0x7a},
|
||||
{0x34, 0x60, 0x7c, 0x9b},
|
||||
{0x21, 0x41, 0x7d, 0xf9},
|
||||
},
|
||||
})
|
||||
filterAdded <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
<-allFiltersAdded
|
||||
}
|
||||
|
||||
func (s *WhisperTestSuite) TestSelectAccount() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
whisperService, err := s.Backend.StatusNode().WhisperService()
|
||||
s.NoError(err)
|
||||
|
||||
// create account 1
|
||||
_, accountInfo1, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
s.NoError(err)
|
||||
|
||||
// create account 2
|
||||
_, accountInfo2, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account2.Password)
|
||||
s.NoError(err)
|
||||
|
||||
// make sure that identities are not injected yet
|
||||
s.False(whisperService.HasKeyPair(accountInfo1.ChatPubKey), "identity already present in whisper")
|
||||
s.False(whisperService.HasKeyPair(accountInfo2.ChatPubKey), "identity already present in whisper")
|
||||
|
||||
// try selecting with wrong password
|
||||
err = s.Backend.SelectAccount(buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, "wrongpassword"))
|
||||
s.NotNil(err)
|
||||
|
||||
// select account 1
|
||||
err = s.Backend.SelectAccount(buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, TestConfig.Account1.Password))
|
||||
s.NoError(err)
|
||||
s.True(whisperService.HasKeyPair(accountInfo1.ChatPubKey), "identity not injected in whisper")
|
||||
|
||||
// select account 2, make sure that previous account is wiped out from Whisper cache
|
||||
s.False(whisperService.HasKeyPair(accountInfo2.ChatPubKey), "identity already present in whisper")
|
||||
s.NoError(s.Backend.SelectAccount(buildLoginParams(accountInfo2.WalletAddress, accountInfo2.ChatAddress, TestConfig.Account2.Password)))
|
||||
s.True(whisperService.HasKeyPair(accountInfo2.ChatPubKey), "identity not injected into whisper")
|
||||
}
|
||||
|
||||
func (s *WhisperTestSuite) TestLogout() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
whisperService, err := s.Backend.StatusNode().WhisperService()
|
||||
s.NoError(err)
|
||||
|
||||
// create an account
|
||||
_, accountInfo, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
s.NoError(err)
|
||||
|
||||
// make sure that identity doesn't exist (yet) in Whisper
|
||||
s.False(whisperService.HasKeyPair(accountInfo.ChatPubKey), "identity already present in whisper")
|
||||
// it should exist only after selecting an account
|
||||
s.NoError(s.Backend.SelectAccount(buildLoginParams(accountInfo.WalletAddress, accountInfo.ChatAddress, TestConfig.Account1.Password)))
|
||||
s.True(whisperService.HasKeyPair(accountInfo.ChatPubKey), "identity not injected into whisper")
|
||||
|
||||
s.NoError(s.Backend.Logout())
|
||||
s.False(whisperService.HasKeyPair(accountInfo.ChatPubKey), "identity not cleared from whisper")
|
||||
}
|
||||
|
||||
func (s *WhisperTestSuite) TestSelectedAccountOnRestart() {
|
||||
s.StartTestBackend()
|
||||
|
||||
// we need to make sure that selected account is injected as identity into Whisper
|
||||
whisperService := s.WhisperService()
|
||||
|
||||
// create test accounts
|
||||
_, accountInfo1, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
s.NoError(err)
|
||||
_, accountInfo2, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account2.Password)
|
||||
s.NoError(err)
|
||||
|
||||
// make sure that identity is not (yet injected)
|
||||
s.False(whisperService.HasKeyPair(accountInfo1.ChatPubKey), "identity already present in whisper")
|
||||
|
||||
// make sure that no wallet account is selected by default
|
||||
selectedWalletAccount, err := s.Backend.AccountManager().MainAccountAddress()
|
||||
s.EqualError(account.ErrNoAccountSelected, err.Error(), "account selected, but should not be")
|
||||
s.Equal(types.Address{}, selectedWalletAccount)
|
||||
|
||||
// make sure that no chat account is selected by default
|
||||
selectedChatAccount, err := s.Backend.AccountManager().SelectedChatAccount()
|
||||
s.EqualError(account.ErrNoAccountSelected, err.Error(), "account selected, but should not be")
|
||||
s.Nil(selectedChatAccount)
|
||||
|
||||
// select account with wrong password
|
||||
err = s.Backend.SelectAccount(buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, "wrongPassword"))
|
||||
expectedErr := errors.New("cannot retrieve a valid key for a given account: could not decrypt key with given password")
|
||||
s.EqualError(expectedErr, err.Error())
|
||||
|
||||
// select account with right password
|
||||
s.NoError(s.Backend.SelectAccount(buildLoginParams(accountInfo1.WalletAddress, accountInfo1.ChatAddress, TestConfig.Account1.Password)))
|
||||
selectedChatAccount1, err := s.Backend.AccountManager().SelectedChatAccount()
|
||||
s.NoError(err)
|
||||
selectedChatPubKey1 := types.EncodeHex(crypto.FromECDSAPub(&selectedChatAccount1.AccountKey.PrivateKey.PublicKey))
|
||||
s.Equal(selectedChatPubKey1, accountInfo1.ChatPubKey)
|
||||
s.True(whisperService.HasKeyPair(s.Backend.SelectedAccountKeyID()), "identity not injected into whisper")
|
||||
|
||||
// select another account, make sure that previous account is wiped out from Whisper cache
|
||||
previousKeyID := s.Backend.SelectedAccountKeyID()
|
||||
s.NoError(s.Backend.SelectAccount(buildLoginParams(accountInfo2.WalletAddress, accountInfo2.ChatAddress, TestConfig.Account2.Password)))
|
||||
selectedChatAccount2, err := s.Backend.AccountManager().SelectedChatAccount()
|
||||
s.NoError(err)
|
||||
selectedChatPubKey2 := types.EncodeHex(crypto.FromECDSAPub(&selectedChatAccount2.AccountKey.PrivateKey.PublicKey))
|
||||
s.Equal(selectedChatPubKey2, accountInfo2.ChatPubKey)
|
||||
s.True(whisperService.HasKeyPair(s.Backend.SelectedAccountKeyID()), "identity not injected into whisper")
|
||||
s.False(whisperService.HasKeyPair(previousKeyID), "identity should be removed, but it is still present in whisper")
|
||||
|
||||
// stop node (and all of its sub-protocols)
|
||||
nodeConfig := s.Backend.StatusNode().Config()
|
||||
s.NotNil(nodeConfig)
|
||||
preservedNodeConfig := *nodeConfig
|
||||
s.NoError(s.Backend.StopNode())
|
||||
|
||||
// resume node
|
||||
s.Require().NoError(s.Backend.StartNode(&preservedNodeConfig))
|
||||
|
||||
// re-check selected account (account2 MUST be selected)
|
||||
selectedWalletAccount, err = s.Backend.AccountManager().MainAccountAddress()
|
||||
s.NoError(err)
|
||||
s.NotNil(selectedWalletAccount)
|
||||
s.Equal(selectedWalletAccount.String(), accountInfo2.WalletAddress, "incorrect wallet address selected")
|
||||
selectedChatAccount, err = s.Backend.AccountManager().SelectedChatAccount()
|
||||
s.NoError(err)
|
||||
s.NotNil(selectedChatAccount)
|
||||
s.Equal(selectedChatAccount.Address.Hex(), accountInfo2.ChatAddress, "incorrect chat address selected")
|
||||
|
||||
// make sure that Whisper gets identity re-injected
|
||||
whisperService = s.WhisperService()
|
||||
s.True(whisperService.HasKeyPair(s.Backend.SelectedAccountKeyID()), "identity not injected into whisper")
|
||||
s.False(whisperService.HasKeyPair(previousKeyID), "identity should not be present, but it is still present in whisper")
|
||||
|
||||
// now restart node using RestartNode() method, and make sure that account is still available
|
||||
s.RestartTestNode()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
whisperService = s.WhisperService()
|
||||
s.True(whisperService.HasKeyPair(s.Backend.SelectedAccountKeyID()), "identity not injected into whisper")
|
||||
s.False(whisperService.HasKeyPair(previousKeyID), "identity should not be present, but it is still present in whisper")
|
||||
|
||||
// now logout, and make sure that on restart no account is selected (i.e. logout works properly)
|
||||
s.NoError(s.Backend.Logout())
|
||||
s.RestartTestNode()
|
||||
whisperService = s.WhisperService()
|
||||
s.False(whisperService.HasKeyPair(s.Backend.SelectedAccountKeyID()), "identity not injected into whisper")
|
||||
s.False(whisperService.HasKeyPair(previousKeyID), "identity should not be present, but it is still present in whisper")
|
||||
|
||||
selectedWalletAccount, err = s.Backend.AccountManager().MainAccountAddress()
|
||||
s.EqualError(account.ErrNoAccountSelected, err.Error())
|
||||
s.Equal(types.Address{}, selectedWalletAccount)
|
||||
|
||||
selectedChatAccount, err = s.Backend.AccountManager().SelectedChatAccount()
|
||||
s.EqualError(account.ErrNoAccountSelected, err.Error())
|
||||
s.Nil(selectedChatAccount)
|
||||
}
|
||||
|
||||
func (s *WhisperTestSuite) TestSelectedChatKeyIsUsedInWhisper() {
|
||||
s.StartTestBackend()
|
||||
defer s.StopTestBackend()
|
||||
|
||||
whisperService, err := s.Backend.StatusNode().WhisperService()
|
||||
s.NoError(err)
|
||||
|
||||
// create an account
|
||||
_, accountInfo, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password)
|
||||
s.NoError(err)
|
||||
|
||||
// select account
|
||||
s.NoError(s.Backend.SelectAccount(buildLoginParams(accountInfo.WalletAddress, accountInfo.ChatAddress, TestConfig.Account1.Password)))
|
||||
|
||||
// Get the chat account
|
||||
selectedChatAccount, err := s.Backend.AccountManager().SelectedChatAccount()
|
||||
s.NoError(err)
|
||||
|
||||
// chat key should be injected in whisper
|
||||
selectedChatPubKey := types.EncodeHex(crypto.FromECDSAPub(&selectedChatAccount.AccountKey.PrivateKey.PublicKey))
|
||||
s.True(whisperService.HasKeyPair(selectedChatPubKey), "identity not injected in whisper")
|
||||
}
|
Loading…
Reference in New Issue