package api import ( "context" "database/sql" "errors" "fmt" "math/big" "os" "path/filepath" "strings" "sync" "time" "github.com/imdario/mergo" "github.com/ethereum/go-ethereum/common/hexutil" ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" signercore "github.com/ethereum/go-ethereum/signer/core" "github.com/status-im/status-go/account" "github.com/status-im/status-go/appdatabase" "github.com/status-im/status-go/connection" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/logutils" "github.com/status-im/status-go/multiaccounts" "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/multiaccounts/settings" "github.com/status-im/status-go/node" "github.com/status-im/status-go/nodecfg" "github.com/status-im/status-go/params" "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/services/ext" "github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/typeddata" "github.com/status-im/status-go/signal" "github.com/status-im/status-go/transactions" ) var ( // ErrWhisperClearIdentitiesFailure clearing whisper identities has failed. ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities") // ErrWhisperIdentityInjectionFailure injecting whisper identities has failed. ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper") // ErrWakuIdentityInjectionFailure injecting whisper identities has failed. ErrWakuIdentityInjectionFailure = errors.New("failed to inject identity into waku") // ErrUnsupportedRPCMethod is for methods not supported by the RPC interface ErrUnsupportedRPCMethod = errors.New("method is unsupported by RPC interface") // ErrRPCClientUnavailable is returned if an RPC client can't be retrieved. // This is a normal situation when a node is stopped. ErrRPCClientUnavailable = errors.New("JSON-RPC client is unavailable") // ErrDBNotAvailable is returned if a method is called before the DB is available for usage ErrDBNotAvailable = errors.New("DB is unavailable") // ErrConfigNotAvailable is returned if a method is called before the nodeconfig is set ErrConfigNotAvailable = errors.New("NodeConfig is not available") ) var _ StatusBackend = (*GethStatusBackend)(nil) // GethStatusBackend implements the Status.im service over go-ethereum type GethStatusBackend struct { mu sync.Mutex // rootDataDir is the same for all networks. rootDataDir string appDB *sql.DB config *params.NodeConfig statusNode *node.StatusNode personalAPI *personal.PublicAPI multiaccountsDB *multiaccounts.Database account *multiaccounts.Account accountManager *account.GethManager transactor *transactions.Transactor connectionState connection.State appState appState selectedAccountKeyID string log log.Logger allowAllRPC bool // used only for tests, disables api method restrictions } // NewGethStatusBackend create a new GethStatusBackend instance func NewGethStatusBackend() *GethStatusBackend { defer log.Info("Status backend initialized", "backend", "geth", "version", params.Version, "commit", params.GitCommit) backend := &GethStatusBackend{} backend.initialize() return backend } func (b *GethStatusBackend) initialize() { accountManager := account.NewGethManager() transactor := transactions.NewTransactor() personalAPI := personal.NewAPI() statusNode := node.New(transactor) b.statusNode = statusNode b.accountManager = accountManager b.transactor = transactor b.personalAPI = personalAPI b.statusNode.SetMultiaccountsDB(b.multiaccountsDB) b.log = log.New("package", "status-go/api.GethStatusBackend") } // StatusNode returns reference to node manager func (b *GethStatusBackend) StatusNode() *node.StatusNode { return b.statusNode } // AccountManager returns reference to account manager func (b *GethStatusBackend) AccountManager() *account.GethManager { return b.accountManager } // Transactor returns reference to a status transactor func (b *GethStatusBackend) Transactor() *transactions.Transactor { return b.transactor } // SelectedAccountKeyID returns a Whisper key ID of the selected chat key pair. func (b *GethStatusBackend) SelectedAccountKeyID() string { return b.selectedAccountKeyID } // IsNodeRunning confirm that node is running func (b *GethStatusBackend) IsNodeRunning() bool { return b.statusNode.IsRunning() } // StartNode start Status node, fails if node is already started func (b *GethStatusBackend) StartNode(config *params.NodeConfig) error { b.mu.Lock() defer b.mu.Unlock() if err := b.startNode(config); err != nil { signal.SendNodeCrashed(err) return err } return nil } func (b *GethStatusBackend) UpdateRootDataDir(datadir string) { b.mu.Lock() defer b.mu.Unlock() b.rootDataDir = datadir } func (b *GethStatusBackend) OpenAccounts() error { b.mu.Lock() defer b.mu.Unlock() if b.multiaccountsDB != nil { return nil } db, err := multiaccounts.InitializeDB(filepath.Join(b.rootDataDir, "accounts.sql")) if err != nil { b.log.Error("failed to initialize accounts db", "err", err) return err } b.multiaccountsDB = db // Probably we should iron out a bit better how to create/dispose of the status-service b.statusNode.SetMultiaccountsDB(db) return nil } func (b *GethStatusBackend) GetAccounts() ([]multiaccounts.Account, error) { b.mu.Lock() defer b.mu.Unlock() if b.multiaccountsDB == nil { return nil, errors.New("accounts db wasn't initialized") } return b.multiaccountsDB.GetAccounts() } func (b *GethStatusBackend) SaveAccount(account multiaccounts.Account) error { b.mu.Lock() defer b.mu.Unlock() if b.multiaccountsDB == nil { return errors.New("accounts db wasn't initialized") } return b.multiaccountsDB.SaveAccount(account) } func (b *GethStatusBackend) DeleteMultiaccount(keyUID string, keyStoreDir string) error { b.mu.Lock() defer b.mu.Unlock() if b.multiaccountsDB == nil { return errors.New("accounts db wasn't initialized") } err := b.multiaccountsDB.DeleteAccount(keyUID) if err != nil { return err } dbFiles := []string{ filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", keyUID)), filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql-shm", keyUID)), filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql-wal", keyUID)), filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", keyUID)), filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db-shm", keyUID)), filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db-wal", keyUID)), } for _, path := range dbFiles { if _, err := os.Stat(path); err == nil { err = os.Remove(path) if err != nil { return err } } } return os.RemoveAll(keyStoreDir) } func (b *GethStatusBackend) DeleteImportedKey(address, password, keyStoreDir string) error { b.mu.Lock() defer b.mu.Unlock() err := filepath.Walk(keyStoreDir, func(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err } if strings.Contains(fileInfo.Name(), address) { _, err := b.accountManager.VerifyAccountPassword(keyStoreDir, "0x"+address, password) if err != nil { b.log.Error("failed to verify account", "account", address, "error", err) return err } return os.Remove(path) } return nil }) return err } func (b *GethStatusBackend) ensureAppDBOpened(account multiaccounts.Account, password string) (err error) { b.mu.Lock() defer b.mu.Unlock() if b.appDB != nil { return nil } if len(b.rootDataDir) == 0 { return errors.New("root datadir wasn't provided") } // Migrate file path to fix issue https://github.com/status-im/status-go/issues/2027 oldPath := filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", account.KeyUID)) newPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", account.KeyUID)) _, err = os.Stat(oldPath) if err == nil { err := os.Rename(oldPath, newPath) if err != nil { return err } // rename journals as well, but ignore errors _ = os.Rename(oldPath+"-shm", newPath+"-shm") _ = os.Rename(oldPath+"-wal", newPath+"-wal") } b.appDB, err = appdatabase.InitializeDB(newPath, password) if err != nil { b.log.Error("failed to initialize db", "err", err) return err } b.statusNode.SetAppDB(b.appDB) return nil } func (b *GethStatusBackend) setupLogSettings() error { logSettings := logutils.LogSettings{ Enabled: b.config.LogEnabled, MobileSystem: b.config.LogMobileSystem, Level: b.config.LogLevel, File: b.config.LogFile, MaxSize: b.config.LogMaxSize, MaxBackups: b.config.LogMaxBackups, CompressRotated: b.config.LogCompressRotated, } if err := logutils.OverrideRootLogWithConfig(logSettings, false); err != nil { return err } return nil } // StartNodeWithKey instead of loading addresses from database this method derives address from key // and uses it in application. // TODO: we should use a proper struct with optional values instead of duplicating the regular functions // with small variants for keycard, this created too many bugs func (b *GethStatusBackend) startNodeWithKey(acc multiaccounts.Account, password string, keyHex string) error { err := b.ensureAppDBOpened(acc, password) if err != nil { return err } err = b.loadNodeConfig(nil) if err != nil { return err } err = b.setupLogSettings() if err != nil { return err } b.account = &acc accountsDB, err := accounts.NewDB(b.appDB) if err != nil { return err } walletAddr, err := accountsDB.GetWalletAddress() if err != nil { return err } watchAddrs, err := accountsDB.GetAddresses() if err != nil { return err } chatKey, err := ethcrypto.HexToECDSA(keyHex) if err != nil { return err } err = b.StartNode(b.config) if err != nil { return err } if err := b.accountManager.SetChatAccount(chatKey); err != nil { return err } _, err = b.accountManager.SelectedChatAccount() if err != nil { return err } b.accountManager.SetAccountAddresses(walletAddr, watchAddrs...) err = b.injectAccountsIntoServices() if err != nil { return err } err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix()) if err != nil { return err } return nil } func (b *GethStatusBackend) StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string) error { err := b.startNodeWithKey(acc, password, keyHex) if err != nil { // Stop node for clean up _ = b.StopNode() } signal.SendLoggedIn(err) return err } func (b *GethStatusBackend) OverwriteNodeConfigValues(conf *params.NodeConfig, n *params.NodeConfig) (*params.NodeConfig, error) { if err := mergo.Merge(conf, n, mergo.WithOverride); err != nil { return nil, err } conf.Networks = n.Networks if err := b.saveNodeConfig(conf); err != nil { return nil, err } return conf, nil } func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, password string, inputNodeCfg *params.NodeConfig) error { err := b.ensureAppDBOpened(acc, password) if err != nil { return err } err = b.loadNodeConfig(inputNodeCfg) if err != nil { return err } err = b.setupLogSettings() if err != nil { return err } b.account = &acc accountsDB, err := accounts.NewDB(b.appDB) if err != nil { return err } chatAddr, err := accountsDB.GetChatAddress() if err != nil { return err } walletAddr, err := accountsDB.GetWalletAddress() if err != nil { return err } watchAddrs, err := accountsDB.GetWalletAddresses() if err != nil { return err } login := account.LoginParams{ Password: password, ChatAddress: chatAddr, WatchAddresses: watchAddrs, MainAccount: walletAddr, } err = b.StartNode(b.config) if err != nil { return err } err = b.SelectAccount(login) if err != nil { return err } err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix()) if err != nil { return err } return nil } func (b *GethStatusBackend) MigrateKeyStoreDir(acc multiaccounts.Account, password, oldDir, newDir string) error { err := b.ensureAppDBOpened(acc, password) if err != nil { return err } accountDB, err := accounts.NewDB(b.appDB) if err != nil { return err } accounts, err := accountDB.GetAccounts() if err != nil { return err } settings, err := accountDB.GetSettings() if err != nil { return err } addresses := []string{settings.EIP1581Address.Hex(), settings.WalletRootAddress.Hex()} for _, account := range accounts { addresses = append(addresses, account.Address.Hex()) } err = b.accountManager.MigrateKeyStoreDir(oldDir, newDir, addresses) if err != nil { return err } return nil } func (b *GethStatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string, nodecfg *params.NodeConfig) error { err := b.startNodeWithAccount(acc, password, nodecfg) if err != nil { // Stop node for clean up _ = b.StopNode() } signal.SendLoggedIn(err) return err } func (b *GethStatusBackend) ExportUnencryptedDatabase(acc multiaccounts.Account, password, directory string) error { b.mu.Lock() defer b.mu.Unlock() if b.appDB != nil { return nil } if len(b.rootDataDir) == 0 { return errors.New("root datadir wasn't provided") } // Migrate file path to fix issue https://github.com/status-im/status-go/issues/2027 oldPath := filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", acc.KeyUID)) newPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", acc.KeyUID)) _, err := os.Stat(oldPath) if err == nil { err := os.Rename(oldPath, newPath) if err != nil { return err } // rename journals as well, but ignore errors _ = os.Rename(oldPath+"-shm", newPath+"-shm") _ = os.Rename(oldPath+"-wal", newPath+"-wal") } err = appdatabase.DecryptDatabase(newPath, directory, password) if err != nil { b.log.Error("failed to initialize db", "err", err) return err } return nil } func (b *GethStatusBackend) ImportUnencryptedDatabase(acc multiaccounts.Account, password, databasePath string) error { b.mu.Lock() defer b.mu.Unlock() if b.appDB != nil { return nil } if len(b.rootDataDir) == 0 { return errors.New("root datadir wasn't provided") } path := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", acc.KeyUID)) err := appdatabase.EncryptDatabase(databasePath, path, password) if err != nil { b.log.Error("failed to initialize db", "err", err) return err } return nil } func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password string, newPassword string) error { dbPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", keyUID)) config := b.StatusNode().Config() keyDir := config.KeyStoreDir err := b.accountManager.ReEncryptKeyStoreDir(keyDir, password, newPassword) if err != nil { return fmt.Errorf("ReEncryptKeyStoreDir error: %v", err) } err = appdatabase.ChangeDatabasePassword(dbPath, password, newPassword) if err != nil { // couldn't change db password so undo keystore changes to mainitain consistency _ = b.accountManager.ReEncryptKeyStoreDir(keyDir, newPassword, password) return err } return nil } func (b *GethStatusBackend) ConvertToKeycardAccount(keyStoreDir string, account multiaccounts.Account, s settings.Settings, password string, newPassword string) error { err := b.multiaccountsDB.UpdateAccountKeycardPairing(account) if err != nil { return err } err = b.ensureAppDBOpened(account, password) if err != nil { return err } accountDB, err := accounts.NewDB(b.appDB) if err != nil { return err } err = accountDB.SaveSettingField(settings.KeycardInstanceUID, s.KeycardInstanceUID) if err != nil { return err } err = accountDB.SaveSettingField(settings.KeycardPairedOn, s.KeycardPAiredOn) if err != nil { return err } err = accountDB.SaveSettingField(settings.KeycardPairing, s.KeycardPairing) if err != nil { return err } err = accountDB.SaveSettingField(settings.Mnemonic, nil) if err != nil { return err } err = accountDB.DeleteSeedAndKeyAccounts() if err != nil { return err } dbPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", account.KeyUID)) err = appdatabase.ChangeDatabasePassword(dbPath, password, newPassword) if err != nil { return err } err = b.closeAppDB() if err != nil { return err } return os.RemoveAll(keyStoreDir) } func (b *GethStatusBackend) VerifyDatabasePassword(keyUID string, password string) error { err := b.ensureAppDBOpened(multiaccounts.Account{KeyUID: keyUID}, password) if err != nil { return err } accountsDB, err := accounts.NewDB(b.appDB) if err != nil { return err } _, err = accountsDB.GetWalletAddress() if err != nil { return err } err = b.closeAppDB() if err != nil { return err } return nil } func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey(acc multiaccounts.Account, password string, settings settings.Settings, nodecfg *params.NodeConfig, subaccs []*accounts.Account, keyHex string) error { err := b.SaveAccount(acc) if err != nil { return err } err = b.ensureAppDBOpened(acc, password) if err != nil { return err } err = b.saveAccountsAndSettings(settings, nodecfg, subaccs) if err != nil { return err } return b.StartNodeWithKey(acc, password, keyHex) } // StartNodeWithAccountAndInitialConfig is used after account and config was generated. // In current setup account name and config is generated on the client side. Once/if it will be generated on // status-go side this flow can be simplified. func (b *GethStatusBackend) StartNodeWithAccountAndInitialConfig( account multiaccounts.Account, password string, settings settings.Settings, nodecfg *params.NodeConfig, subaccs []*accounts.Account, ) error { err := b.SaveAccount(account) if err != nil { return err } err = b.ensureAppDBOpened(account, password) if err != nil { return err } err = b.saveAccountsAndSettings(settings, nodecfg, subaccs) if err != nil { return err } return b.StartNodeWithAccount(account, password, nil) } func (b *GethStatusBackend) saveAccountsAndSettings(settings settings.Settings, nodecfg *params.NodeConfig, subaccs []*accounts.Account) error { b.mu.Lock() defer b.mu.Unlock() accdb, err := accounts.NewDB(b.appDB) if err != nil { return err } err = accdb.CreateSettings(settings, *nodecfg) if err != nil { return err } return accdb.SaveAccounts(subaccs) } func (b *GethStatusBackend) loadNodeConfig(inputNodeCfg *params.NodeConfig) error { b.mu.Lock() defer b.mu.Unlock() conf, err := nodecfg.GetNodeConfigFromDB(b.appDB) if err != nil { return err } if inputNodeCfg != nil { conf, err = b.OverwriteNodeConfigValues(conf, inputNodeCfg) if err != nil { return err } } // Start WakuV1 if WakuV2 is not enabled conf.WakuConfig.Enabled = !conf.WakuV2Config.Enabled // NodeConfig.Version should be taken from params.Version // which is set at the compile time. // What's cached is usually outdated so we overwrite it here. conf.Version = params.Version conf.RootDataDir = b.rootDataDir conf.DataDir = filepath.Join(b.rootDataDir, conf.DataDir) conf.ShhextConfig.BackupDisabledDataDir = filepath.Join(b.rootDataDir, conf.ShhextConfig.BackupDisabledDataDir) if len(conf.LogDir) == 0 { conf.LogFile = filepath.Join(b.rootDataDir, conf.LogFile) } else { conf.LogFile = filepath.Join(conf.LogDir, conf.LogFile) } conf.KeyStoreDir = filepath.Join(b.rootDataDir, conf.KeyStoreDir) b.config = conf return nil } func (b *GethStatusBackend) saveNodeConfig(n *params.NodeConfig) error { err := nodecfg.SaveNodeConfig(b.appDB, n) if err != nil { return err } return nil } func (b *GethStatusBackend) GetNodeConfig() (*params.NodeConfig, error) { return nodecfg.GetNodeConfigFromDB(b.appDB) } func (b *GethStatusBackend) startNode(config *params.NodeConfig) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("node crashed on start: %v", err) } }() b.log.Info("status-go version details", "version", params.Version, "commit", params.GitCommit) b.log.Debug("starting node with config", "config", config) // Update config with some defaults. if err := config.UpdateWithDefaults(); err != nil { return err } // Updating node config b.config = config b.log.Debug("updated config with defaults", "config", config) // Start by validating configuration if err := config.Validate(); err != nil { return err } manager := b.accountManager.GetManager() if manager == nil { return errors.New("ethereum accounts.Manager is nil") } if err = b.statusNode.StartWithOptions(config, node.StartOptions{ // The peers discovery protocols are started manually after // `node.ready` signal is sent. // It was discussed in https://github.com/status-im/status-go/pull/1333. StartDiscovery: false, AccountsManager: manager, }); err != nil { return } signal.SendNodeStarted() b.transactor.SetNetworkID(config.NetworkID) b.transactor.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout) b.personalAPI.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout) if err = b.registerHandlers(); err != nil { b.log.Error("Handler registration failed", "err", err) return } b.log.Info("Handlers registered") // Handle a case when a node is stopped and resumed. // If there is no account selected, an error is returned. if _, err := b.accountManager.SelectedChatAccount(); err == nil { if err := b.injectAccountsIntoServices(); err != nil { return err } } else if err != account.ErrNoAccountSelected { return err } signal.SendNodeReady() if err := b.statusNode.StartDiscovery(); err != nil { return err } return nil } // StopNode stop Status node. Stopped node cannot be resumed. func (b *GethStatusBackend) StopNode() error { b.mu.Lock() defer b.mu.Unlock() return b.stopNode() } func (b *GethStatusBackend) stopNode() error { if b.statusNode == nil || !b.IsNodeRunning() { return nil } defer signal.SendNodeStopped() return b.statusNode.Stop() } // RestartNode restart running Status node, fails if node is not running func (b *GethStatusBackend) RestartNode() error { b.mu.Lock() defer b.mu.Unlock() if !b.IsNodeRunning() { return node.ErrNoRunningNode } if err := b.stopNode(); err != nil { return err } return b.startNode(b.config) } // ResetChainData remove chain data from data directory. // Node is stopped, and new node is started, with clean data directory. func (b *GethStatusBackend) ResetChainData() error { b.mu.Lock() defer b.mu.Unlock() if err := b.stopNode(); err != nil { return err } // config is cleaned when node is stopped if err := b.statusNode.ResetChainData(b.config); err != nil { return err } signal.SendChainDataRemoved() return b.startNode(b.config) } // CallRPC executes public RPC requests on node's in-proc RPC server. func (b *GethStatusBackend) CallRPC(inputJSON string) (string, error) { client := b.statusNode.RPCClient() if client == nil { return "", ErrRPCClientUnavailable } return client.CallRaw(inputJSON), nil } // CallPrivateRPC executes public and private RPC requests on node's in-proc RPC server. func (b *GethStatusBackend) CallPrivateRPC(inputJSON string) (string, error) { client := b.statusNode.RPCClient() if client == nil { return "", ErrRPCClientUnavailable } return client.CallRaw(inputJSON), nil } // SendTransaction creates a new transaction and waits until it's complete. func (b *GethStatusBackend) SendTransaction(sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) { verifiedAccount, err := b.getVerifiedWalletAccount(sendArgs.From.String(), password) if err != nil { return hash, err } hash, err = b.transactor.SendTransaction(sendArgs, verifiedAccount) if err != nil { return } go b.statusNode.RPCFiltersService().TriggerTransactionSentToUpstreamEvent(hash) return } func (b *GethStatusBackend) SendTransactionWithChainID(chainID uint64, sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) { verifiedAccount, err := b.getVerifiedWalletAccount(sendArgs.From.String(), password) if err != nil { return hash, err } hash, err = b.transactor.SendTransactionWithChainID(chainID, sendArgs, verifiedAccount) if err != nil { return } go b.statusNode.RPCFiltersService().TriggerTransactionSentToUpstreamEvent(hash) return } func (b *GethStatusBackend) SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash types.Hash, err error) { hash, err = b.transactor.SendTransactionWithSignature(sendArgs, sig) if err != nil { return } go b.statusNode.RPCFiltersService().TriggerTransactionSentToUpstreamEvent(hash) return } // HashTransaction validate the transaction and returns new sendArgs and the transaction hash. func (b *GethStatusBackend) HashTransaction(sendArgs transactions.SendTxArgs) (transactions.SendTxArgs, types.Hash, error) { return b.transactor.HashTransaction(sendArgs) } // SignMessage checks the pwd vs the selected account and passes on the signParams // to personalAPI for message signature func (b *GethStatusBackend) SignMessage(rpcParams personal.SignParams) (types.HexBytes, error) { verifiedAccount, err := b.getVerifiedWalletAccount(rpcParams.Address, rpcParams.Password) if err != nil { return types.HexBytes{}, err } return b.personalAPI.Sign(rpcParams, verifiedAccount) } // Recover calls the personalAPI to return address associated with the private // key that was used to calculate the signature in the message func (b *GethStatusBackend) Recover(rpcParams personal.RecoverParams) (types.Address, error) { return b.personalAPI.Recover(rpcParams) } // SignTypedData accepts data and password. Gets verified account and signs typed data. func (b *GethStatusBackend) SignTypedData(typed typeddata.TypedData, address string, password string) (types.HexBytes, error) { account, err := b.getVerifiedWalletAccount(address, password) if err != nil { return types.HexBytes{}, err } chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID) sig, err := typeddata.Sign(typed, account.AccountKey.PrivateKey, chain) if err != nil { return types.HexBytes{}, err } return types.HexBytes(sig), err } // SignTypedDataV4 accepts data and password. Gets verified account and signs typed data. func (b *GethStatusBackend) SignTypedDataV4(typed signercore.TypedData, address string, password string) (types.HexBytes, error) { account, err := b.getVerifiedWalletAccount(address, password) if err != nil { return types.HexBytes{}, err } chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID) sig, err := typeddata.SignTypedDataV4(typed, account.AccountKey.PrivateKey, chain) if err != nil { return types.HexBytes{}, err } return types.HexBytes(sig), err } // HashTypedData generates the hash of TypedData. func (b *GethStatusBackend) HashTypedData(typed typeddata.TypedData) (types.Hash, error) { chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID) hash, err := typeddata.ValidateAndHash(typed, chain) if err != nil { return types.Hash{}, err } return types.Hash(hash), err } // HashTypedDataV4 generates the hash of TypedData. func (b *GethStatusBackend) HashTypedDataV4(typed signercore.TypedData) (types.Hash, error) { chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID) hash, err := typeddata.HashTypedDataV4(typed, chain) if err != nil { return types.Hash{}, err } return types.Hash(hash), err } func (b *GethStatusBackend) getVerifiedWalletAccount(address, password string) (*account.SelectedExtKey, error) { config := b.StatusNode().Config() db, err := accounts.NewDB(b.appDB) if err != nil { b.log.Error("failed to create new *Database instance", "error", err) return nil, err } exists, err := db.AddressExists(types.HexToAddress(address)) if err != nil { b.log.Error("failed to query db for a given address", "address", address, "error", err) return nil, err } if !exists { b.log.Error("failed to get a selected account", "err", transactions.ErrInvalidTxSender) return nil, transactions.ErrAccountDoesntExist } key, err := b.accountManager.VerifyAccountPassword(config.KeyStoreDir, address, password) if _, ok := err.(*account.ErrCannotLocateKeyFile); ok { key, err = b.generatePartialAccountKey(db, address, password) if err != nil { return nil, err } } if err != nil { b.log.Error("failed to verify account", "account", address, "error", err) return nil, err } return &account.SelectedExtKey{ Address: key.Address, AccountKey: key, }, nil } func (b *GethStatusBackend) generatePartialAccountKey(db *accounts.Database, address string, password string) (*types.Key, error) { dbPath, err := db.GetPath(types.HexToAddress(address)) path := "m/" + dbPath[strings.LastIndex(dbPath, "/")+1:] if err != nil { b.log.Error("failed to get path for given account address", "account", address, "error", err) return nil, err } rootAddress, err := db.GetWalletRootAddress() if err != nil { return nil, err } info, err := b.accountManager.AccountsGenerator().LoadAccount(rootAddress.Hex(), password) if err != nil { return nil, err } masterID := info.ID accInfosMap, err := b.accountManager.AccountsGenerator().StoreDerivedAccounts(masterID, password, []string{path}) if err != nil { return nil, err } _, key, err := b.accountManager.AddressToDecryptedAccount(accInfosMap[path].Address, password) if err != nil { return nil, err } return key, nil } // registerHandlers attaches Status callback handlers to running node func (b *GethStatusBackend) registerHandlers() error { var clients []*rpc.Client if c := b.StatusNode().RPCClient(); c != nil { clients = append(clients, c) } else { return errors.New("RPC client unavailable") } for _, client := range clients { client.RegisterHandler( params.AccountsMethodName, func(context.Context, uint64, ...interface{}) (interface{}, error) { return b.accountManager.Accounts() }, ) if b.allowAllRPC { // this should only happen in unit-tests, this variable is not available outside this package continue } client.RegisterHandler(params.SendTransactionMethodName, unsupportedMethodHandler) client.RegisterHandler(params.PersonalSignMethodName, unsupportedMethodHandler) client.RegisterHandler(params.PersonalRecoverMethodName, unsupportedMethodHandler) } return nil } func unsupportedMethodHandler(ctx context.Context, chainID uint64, rpcParams ...interface{}) (interface{}, error) { return nil, ErrUnsupportedRPCMethod } // ConnectionChange handles network state changes logic. func (b *GethStatusBackend) ConnectionChange(typ string, expensive bool) { b.mu.Lock() defer b.mu.Unlock() state := connection.State{ Type: connection.NewType(typ), Expensive: expensive, } if typ == connection.None { state.Offline = true } b.log.Info("Network state change", "old", b.connectionState, "new", state) b.connectionState = state b.statusNode.ConnectionChanged(state) // logic of handling state changes here // restart node? force peers reconnect? etc } // AppStateChange handles app state changes (background/foreground). // state values: see https://facebook.github.io/react-native/docs/appstate.html func (b *GethStatusBackend) AppStateChange(state string) { s, err := parseAppState(state) if err != nil { log.Error("AppStateChange failed, ignoring", "error", err) return // and do nothing } b.log.Info("App State changed", "new-state", s) b.appState = s if b.statusNode != nil { wakuext := b.statusNode.WakuExtService() if wakuext != nil { messenger := wakuext.Messenger() if messenger != nil { if s == appStateForeground { messenger.ToForeground() } else { messenger.ToBackground() } } } } // TODO: put node in low-power mode if the app is in background (or inactive) // and normal mode if the app is in foreground. } func (b *GethStatusBackend) StopLocalNotifications() error { if b.statusNode == nil { return nil } return b.statusNode.StopLocalNotifications() } func (b *GethStatusBackend) StartLocalNotifications() error { if b.statusNode == nil { return nil } return b.statusNode.StartLocalNotifications() } // Logout clears whisper identities. func (b *GethStatusBackend) Logout() error { b.mu.Lock() defer b.mu.Unlock() err := b.cleanupServices() if err != nil { return err } err = b.closeAppDB() if err != nil { return err } b.AccountManager().Logout() b.appDB = nil if b.statusNode != nil { if err := b.statusNode.Stop(); err != nil { return err } b.statusNode = nil } // re-initialize the node, at some point we should better manage the lifecycle b.initialize() return nil } // cleanupServices stops parts of services that doesn't managed by a node and removes injected data from services. func (b *GethStatusBackend) cleanupServices() error { b.selectedAccountKeyID = "" if b.statusNode == nil { return nil } return b.statusNode.Cleanup() } func (b *GethStatusBackend) closeAppDB() error { if b.appDB != nil { err := b.appDB.Close() if err != nil { return err } b.appDB = nil return nil } return nil } // SelectAccount selects current wallet and chat accounts, by verifying that each address has corresponding account which can be decrypted // using provided password. Once verification is done, the decrypted chat key is injected into Whisper (as a single identity, // all previous identities are removed). func (b *GethStatusBackend) SelectAccount(loginParams account.LoginParams) error { b.mu.Lock() defer b.mu.Unlock() b.AccountManager().RemoveOnboarding() err := b.accountManager.SelectAccount(loginParams) if err != nil { return err } if loginParams.MultiAccount != nil { b.account = loginParams.MultiAccount } if err := b.injectAccountsIntoServices(); err != nil { return err } return nil } func (b *GethStatusBackend) GetActiveAccount() (*multiaccounts.Account, error) { if b.account == nil { return nil, errors.New("master key account is nil in the GethStatusBackend") } return b.account, nil } func (b *GethStatusBackend) injectAccountsIntoWakuService(w types.WakuKeyManager, st *ext.Service) error { chatAccount, err := b.accountManager.SelectedChatAccount() if err != nil { return err } identity := chatAccount.AccountKey.PrivateKey acc, err := b.GetActiveAccount() if err != nil { return err } if err := w.DeleteKeyPairs(); err != nil { // err is not possible; method return value is incorrect return err } b.selectedAccountKeyID, err = w.AddKeyPair(identity) if err != nil { return ErrWakuIdentityInjectionFailure } if st != nil { if err := st.InitProtocol(b.statusNode.GethNode().Config().Name, identity, b.appDB, b.statusNode.HTTPServer(), b.multiaccountsDB, acc, logutils.ZapLogger()); err != nil { return err } // Set initial connection state st.ConnectionChanged(b.connectionState) messenger := st.Messenger() // Init public status api b.statusNode.StatusPublicService().Init(messenger) b.statusNode.AccountService().Init(messenger) // Init chat service accDB, err := accounts.NewDB(b.appDB) if err != nil { return err } b.statusNode.ChatService(accDB).Init(messenger) } return nil } func (b *GethStatusBackend) injectAccountsIntoServices() error { if b.statusNode.WakuService() != nil { return b.injectAccountsIntoWakuService(b.statusNode.WakuService(), func() *ext.Service { if b.statusNode.WakuExtService() == nil { return nil } return b.statusNode.WakuExtService().Service }()) } if b.statusNode.WakuV2Service() != nil { return b.injectAccountsIntoWakuService(b.statusNode.WakuV2Service(), func() *ext.Service { if b.statusNode.WakuV2ExtService() == nil { return nil } return b.statusNode.WakuV2ExtService().Service }()) } return nil } // ExtractGroupMembershipSignatures extract signatures from tuples of content/signature func (b *GethStatusBackend) ExtractGroupMembershipSignatures(signaturePairs [][2]string) ([]string, error) { return crypto.ExtractSignatures(signaturePairs) } // SignGroupMembership signs a piece of data containing membership information func (b *GethStatusBackend) SignGroupMembership(content string) (string, error) { selectedChatAccount, err := b.accountManager.SelectedChatAccount() if err != nil { return "", err } return crypto.SignStringAsHex(content, selectedChatAccount.AccountKey.PrivateKey) } // SignHash exposes vanilla ECDSA signing for signing a message for Swarm func (b *GethStatusBackend) SignHash(hexEncodedHash string) (string, error) { hash, err := hexutil.Decode(hexEncodedHash) if err != nil { return "", fmt.Errorf("SignHash: could not unmarshal the input: %v", err) } chatAccount, err := b.accountManager.SelectedChatAccount() if err != nil { return "", fmt.Errorf("SignHash: could not select account: %v", err.Error()) } signature, err := ethcrypto.Sign(hash, chatAccount.AccountKey.PrivateKey) if err != nil { return "", fmt.Errorf("SignHash: could not sign the hash: %v", err) } hexEncodedSignature := types.EncodeHex(signature) return hexEncodedSignature, nil } func (b *GethStatusBackend) SwitchFleet(fleet string, conf *params.NodeConfig) error { if b.appDB == nil { return ErrDBNotAvailable } accountDB, err := accounts.NewDB(b.appDB) if err != nil { return err } err = accountDB.SaveSetting("fleet", fleet) if err != nil { return err } err = nodecfg.SaveNodeConfig(b.appDB, conf) if err != nil { return err } waku2 := b.statusNode.WakuV2Service() if waku2 != nil { return waku2.ClearPeerCache() } return nil }