2566 lines
70 KiB
Go
2566 lines
70 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/status-im/status-go/services/ens"
|
|
"github.com/status-im/status-go/sqlite"
|
|
|
|
"github.com/imdario/mergo"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"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/apitypes"
|
|
|
|
"github.com/status-im/status-go/account"
|
|
"github.com/status-im/status-go/account/generator"
|
|
"github.com/status-im/status-go/appdatabase"
|
|
"github.com/status-im/status-go/common/dbsetup"
|
|
"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/images"
|
|
"github.com/status-im/status-go/logutils"
|
|
"github.com/status-im/status-go/multiaccounts"
|
|
"github.com/status-im/status-go/multiaccounts/accounts"
|
|
multiacccommon "github.com/status-im/status-go/multiaccounts/common"
|
|
"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/protocol"
|
|
identityutils "github.com/status-im/status-go/protocol/identity"
|
|
"github.com/status-im/status-go/protocol/identity/colorhash"
|
|
"github.com/status-im/status-go/protocol/requests"
|
|
"github.com/status-im/status-go/rpc"
|
|
"github.com/status-im/status-go/server/pairing/statecontrol"
|
|
"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"
|
|
"github.com/status-im/status-go/walletdatabase"
|
|
)
|
|
|
|
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
|
|
walletDB *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
|
|
LocalPairingStateManager *statecontrol.ProcessStateManager
|
|
}
|
|
|
|
// NewGethStatusBackend create a new GethStatusBackend instance
|
|
func NewGethStatusBackend() *GethStatusBackend {
|
|
defer log.Info("Status backend initialized", "backend", "geth", "version", params.Version, "commit", params.GitCommit, "IpfsGatewayURL", params.IpfsGatewayURL)
|
|
|
|
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")
|
|
b.LocalPairingStateManager = new(statecontrol.ProcessStateManager)
|
|
b.LocalPairingStateManager.SetPairing(false)
|
|
}
|
|
|
|
// 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) GetMultiaccountDB() *multiaccounts.Database {
|
|
return b.multiaccountsDB
|
|
}
|
|
|
|
func (b *GethStatusBackend) InitializeAccounts(rootDirectory string) error {
|
|
b.UpdateRootDataDir(rootDirectory)
|
|
manager := b.AccountManager()
|
|
keystoreDir := filepath.Join(rootDirectory, keystoreRelativePath)
|
|
if err := manager.InitKeystore(keystoreDir); err != nil {
|
|
return err
|
|
}
|
|
return b.OpenAccounts()
|
|
}
|
|
|
|
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)
|
|
|
|
err = b.statusNode.StartMediaServerWithoutDB()
|
|
if err != nil {
|
|
b.log.Error("failed to start media server without app db", "err", err)
|
|
return err
|
|
}
|
|
|
|
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) getAccountByKeyUID(keyUID string) (*multiaccounts.Account, error) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.multiaccountsDB == nil {
|
|
return nil, errors.New("accounts db wasn't initialized")
|
|
}
|
|
as, err := b.multiaccountsDB.GetAccounts()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, acc := range as {
|
|
if acc.KeyUID == keyUID {
|
|
return &acc, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("account with keyUID %s not found", keyUID)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
appDbPath, err := b.getAppDBPath(keyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
walletDbPath, err := b.getWalletDBPath(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)),
|
|
appDbPath,
|
|
appDbPath + "-shm",
|
|
appDbPath + "-wal",
|
|
walletDbPath,
|
|
walletDbPath + "-shm",
|
|
walletDbPath + "-wal",
|
|
}
|
|
for _, path := range dbFiles {
|
|
if _, err := os.Stat(path); err == nil {
|
|
err = os.Remove(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if b.account != nil && b.account.KeyUID == keyUID {
|
|
// reset active account
|
|
b.account = nil
|
|
}
|
|
|
|
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) runDBFileMigrations(account multiaccounts.Account, password string) (string, error) {
|
|
// Migrate file path to fix issue https://github.com/status-im/status-go/issues/2027
|
|
unsupportedPath := filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", account.KeyUID))
|
|
v3Path := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", account.KeyUID))
|
|
v4Path, err := b.getAppDBPath(account.KeyUID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = os.Stat(unsupportedPath)
|
|
if err == nil {
|
|
err := os.Rename(unsupportedPath, v3Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// rename journals as well, but ignore errors
|
|
_ = os.Rename(unsupportedPath+"-shm", v3Path+"-shm")
|
|
_ = os.Rename(unsupportedPath+"-wal", v3Path+"-wal")
|
|
}
|
|
|
|
if _, err = os.Stat(v3Path); err == nil {
|
|
if err := appdatabase.MigrateV3ToV4(v3Path, v4Path, password, account.KDFIterations, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished); err != nil {
|
|
_ = os.Remove(v4Path)
|
|
_ = os.Remove(v4Path + "-shm")
|
|
_ = os.Remove(v4Path + "-wal")
|
|
return "", errors.New("Failed to migrate v3 db to v4: " + err.Error())
|
|
}
|
|
_ = os.Remove(v3Path)
|
|
_ = os.Remove(v3Path + "-shm")
|
|
_ = os.Remove(v3Path + "-wal")
|
|
}
|
|
|
|
return v4Path, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) ensureDBsOpened(account multiaccounts.Account, password string) (err error) {
|
|
// After wallet DB initial migration, the tables moved to wallet DB are removed from appDB
|
|
// so better migrate wallet DB first to avoid removal if wallet DB migration fails
|
|
if err = b.ensureWalletDBOpened(account, password); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = b.ensureAppDBOpened(account, password); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
dbFilePath, err := b.runDBFileMigrations(account, password)
|
|
if err != nil {
|
|
return errors.New("Failed to migrate db file: " + err.Error())
|
|
}
|
|
|
|
appdatabase.CurrentAppDBKeyUID = account.KeyUID
|
|
b.appDB, err = appdatabase.InitializeDB(dbFilePath, password, account.KDFIterations)
|
|
if err != nil {
|
|
b.log.Error("failed to initialize db", "err", err.Error())
|
|
return err
|
|
}
|
|
b.statusNode.SetAppDB(b.appDB)
|
|
return nil
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (b *GethStatusBackend) walletDBExists(keyUID string) bool {
|
|
path, err := b.getWalletDBPath(keyUID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return fileExists(path)
|
|
}
|
|
|
|
func (b *GethStatusBackend) appDBExists(keyUID string) bool {
|
|
path, err := b.getAppDBPath(keyUID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return fileExists(path)
|
|
}
|
|
|
|
func (b *GethStatusBackend) ensureWalletDBOpened(account multiaccounts.Account, password string) (err error) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.walletDB != nil {
|
|
return nil
|
|
}
|
|
|
|
dbWalletPath, err := b.getWalletDBPath(account.KeyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.walletDB, err = walletdatabase.InitializeDB(dbWalletPath, password, account.KDFIterations)
|
|
if err != nil {
|
|
b.log.Error("failed to initialize wallet db", "err", err.Error())
|
|
return err
|
|
}
|
|
b.statusNode.SetWalletDB(b.walletDB)
|
|
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, inputNodeCfg *params.NodeConfig) error {
|
|
if acc.KDFIterations == 0 {
|
|
kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(acc.KeyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
acc.KDFIterations = kdfIterations
|
|
}
|
|
|
|
err := b.ensureDBsOpened(acc, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.loadNodeConfig(inputNodeCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.setupLogSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accountsDB, err := accounts.NewDB(b.appDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if acc.ColorHash == nil {
|
|
multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
acc = *multiAccount
|
|
}
|
|
|
|
b.account = &acc
|
|
|
|
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, nodecfg *params.NodeConfig) error {
|
|
err := b.startNodeWithKey(acc, password, keyHex, nodecfg)
|
|
if err != nil {
|
|
// Stop node for clean up
|
|
_ = b.StopNode()
|
|
return err
|
|
}
|
|
// get logged in
|
|
if !b.LocalPairingStateManager.IsPairing() {
|
|
return b.LoggedIn(acc.KeyUID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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) updateAccountColorHashAndColorID(keyUID string, accountsDB *accounts.Database) (*multiaccounts.Account, error) {
|
|
multiAccount, err := b.getAccountByKeyUID(keyUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if multiAccount.ColorHash == nil {
|
|
keypair, err := accountsDB.GetKeypairByKeyUID(keyUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
publicKey := keypair.GetChatPublicKey()
|
|
if publicKey == nil {
|
|
return nil, errors.New("chat public key not found")
|
|
}
|
|
if err = enrichMultiAccountByPublicKey(multiAccount, publicKey); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = b.multiaccountsDB.UpdateAccount(*multiAccount); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return multiAccount, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) overrideNetworks(conf *params.NodeConfig, request *requests.Login) {
|
|
conf.Networks = setRPCs(defaultNetworks, &request.WalletSecretsConfig)
|
|
}
|
|
|
|
func (b *GethStatusBackend) LoginAccount(request *requests.Login) error {
|
|
err := b.loginAccount(request)
|
|
if err != nil {
|
|
// Stop node for clean up
|
|
_ = b.StopNode()
|
|
}
|
|
return b.LoggedIn(request.KeyUID, err)
|
|
}
|
|
|
|
func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
|
|
if err := request.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
password := request.Password
|
|
|
|
acc := multiaccounts.Account{
|
|
KeyUID: request.KeyUID,
|
|
KDFIterations: request.KdfIterations,
|
|
}
|
|
|
|
if acc.KDFIterations == 0 {
|
|
acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber
|
|
}
|
|
|
|
err := b.ensureDBsOpened(acc, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defaultCfg := ¶ms.NodeConfig{
|
|
// why we need this? relate PR: https://github.com/status-im/status-go/pull/4014
|
|
KeycardPairingDataFile: DefaultKeycardPairingDataFile,
|
|
}
|
|
|
|
defaultCfg.WalletConfig = buildWalletConfig(&request.WalletSecretsConfig)
|
|
|
|
err = b.UpdateNodeConfigFleet(acc, password, defaultCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.loadNodeConfig(defaultCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if request.RuntimeLogLevel != "" {
|
|
b.config.LogLevel = request.RuntimeLogLevel
|
|
}
|
|
|
|
if b.config.WakuV2Config.Enabled && request.WakuV2Nameserver != "" {
|
|
b.config.WakuV2Config.Nameserver = request.WakuV2Nameserver
|
|
}
|
|
|
|
b.overrideNetworks(b.config, request)
|
|
|
|
err = b.setupLogSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accountsDB, err := accounts.NewDB(b.appDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.account = multiAccount
|
|
|
|
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 {
|
|
b.log.Info("failed to start node")
|
|
return err
|
|
}
|
|
|
|
err = b.SelectAccount(login)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
|
|
if err != nil {
|
|
b.log.Info("failed to update account")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateNodeConfigFleet loads the fleet from the settings and updates the node configuration
|
|
// If the fleet in settings is empty, or not supported anymore, it will be overridden with the default fleet.
|
|
// In that case settings fleet value remain the same, only runtime node configuration is updated.
|
|
func (b *GethStatusBackend) UpdateNodeConfigFleet(acc multiaccounts.Account, password string, config *params.NodeConfig) error {
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
|
|
err := b.ensureDBsOpened(acc, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accountSettings, err := b.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fleet := accountSettings.GetFleet()
|
|
|
|
if !params.IsFleetSupported(fleet) {
|
|
b.log.Warn("fleet is not supported, overriding with default value",
|
|
"fleet", fleet,
|
|
"defaultFleet", DefaultFleet)
|
|
fleet = DefaultFleet
|
|
}
|
|
|
|
err = SetFleet(fleet, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, password string, inputNodeCfg *params.NodeConfig) error {
|
|
err := b.ensureDBsOpened(acc, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.loadNodeConfig(inputNodeCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.setupLogSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accountsDB, err := accounts.NewDB(b.appDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if acc.ColorHash == nil {
|
|
multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
acc = *multiAccount
|
|
}
|
|
|
|
b.account = &acc
|
|
|
|
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 {
|
|
b.log.Info("failed to start node")
|
|
return err
|
|
}
|
|
|
|
err = b.SelectAccount(login)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
|
|
if err != nil {
|
|
b.log.Info("failed to update account")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) accountsDB() (*accounts.Database, error) {
|
|
return accounts.NewDB(b.appDB)
|
|
}
|
|
|
|
func (b *GethStatusBackend) GetSettings() (*settings.Settings, error) {
|
|
accountsDB, err := b.accountsDB()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
settings, err := accountsDB.GetSettings()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &settings, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) GetEnsUsernames() ([]*ens.UsernameDetail, error) {
|
|
db := ens.NewEnsDatabase(b.appDB)
|
|
removed := false
|
|
return db.GetEnsUsernames(&removed)
|
|
}
|
|
|
|
func (b *GethStatusBackend) MigrateKeyStoreDir(acc multiaccounts.Account, password, oldDir, newDir string) error {
|
|
err := b.ensureDBsOpened(acc, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accountDB, err := accounts.NewDB(b.appDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accounts, err := accountDB.GetActiveAccounts()
|
|
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) Login(keyUID, password string) error {
|
|
return b.startNodeWithAccount(multiaccounts.Account{KeyUID: keyUID}, password, 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()
|
|
}
|
|
// get logged in
|
|
if !b.LocalPairingStateManager.IsPairing() {
|
|
return b.LoggedIn(acc.KeyUID, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (b *GethStatusBackend) LoggedIn(keyUID string, err error) error {
|
|
if err != nil {
|
|
signal.SendLoggedIn(nil, nil, nil, err)
|
|
return err
|
|
}
|
|
settings, err := b.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account, err := b.getAccountByKeyUID(keyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ensUsernames, err := b.GetEnsUsernames()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var ensUsernamesJSON json.RawMessage
|
|
if ensUsernames != nil {
|
|
ensUsernamesJSON, err = json.Marshal(ensUsernames)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
signal.SendLoggedIn(account, settings, ensUsernamesJSON, nil)
|
|
return nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
dbPath, err := b.runDBFileMigrations(acc, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = sqlite.DecryptDB(dbPath, directory, password, acc.KDFIterations)
|
|
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
|
|
}
|
|
|
|
path, err := b.getAppDBPath(acc.KeyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = sqlite.EncryptDB(databasePath, path, password, acc.KDFIterations, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished)
|
|
if err != nil {
|
|
b.log.Error("failed to initialize db", "err", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) reEncryptKeyStoreDir(currentPassword string, newPassword string) error {
|
|
config := b.StatusNode().Config()
|
|
keyDir := ""
|
|
if config == nil {
|
|
keyDir = b.accountManager.Keydir
|
|
} else {
|
|
keyDir = config.KeyStoreDir
|
|
}
|
|
|
|
if keyDir != "" {
|
|
err := b.accountManager.ReEncryptKeyStoreDir(keyDir, currentPassword, newPassword)
|
|
if err != nil {
|
|
return fmt.Errorf("ReEncryptKeyStoreDir error: %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password string, newPassword string) error {
|
|
account, err := b.multiaccountsDB.GetAccount(keyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
internalDbPath, err := dbsetup.GetDBFilename(b.appDB)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get database file name, %w", err)
|
|
}
|
|
|
|
appDBPath, err := b.getAppDBPath(keyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isCurrentAccount := appDBPath == internalDbPath
|
|
|
|
restartNode := func() {
|
|
if isCurrentAccount {
|
|
if err != nil {
|
|
// TODO https://github.com/status-im/status-go/issues/3906
|
|
// Fix restarting node, as it always fails but the error is ignored
|
|
// because UI calls Logout and Quit afterwards. It should not be UI-dependent
|
|
// and should be handled gracefully here if it makes sense to run dummy node after
|
|
// logout
|
|
_ = b.startNodeWithAccount(*account, password, nil)
|
|
} else {
|
|
_ = b.startNodeWithAccount(*account, newPassword, nil)
|
|
}
|
|
}
|
|
}
|
|
defer restartNode()
|
|
|
|
logout := func() {
|
|
if isCurrentAccount {
|
|
_ = b.Logout()
|
|
}
|
|
}
|
|
noLogout := func() {}
|
|
|
|
// First change app DB password, because it also reencrypts the keystore,
|
|
// otherwise if we call changeWalletDbPassword first and logout, we will fail
|
|
// to reencrypt the keystore
|
|
err = b.changeAppDBPassword(account, logout, password, newPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Already logged out but pass a param to decouple the logic for testing
|
|
err = b.changeWalletDBPassword(account, noLogout, password, newPassword)
|
|
if err != nil {
|
|
// Revert the password to original
|
|
err2 := b.changeAppDBPassword(account, noLogout, newPassword, password)
|
|
if err2 != nil {
|
|
log.Error("failed to revert app db password", "err", err2)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) changeAppDBPassword(account *multiaccounts.Account, logout func(), password string, newPassword string) error {
|
|
tmpDbPath, cleanup, err := b.createTempDBFile("v4.db")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
dbPath, err := b.getAppDBPath(account.KeyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Exporting database to a temporary file with a new password
|
|
err = sqlite.ExportDB(dbPath, password, account.KDFIterations, tmpDbPath, newPassword, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.reEncryptKeyStoreDir(password, newPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Replacing the old database with the new one requires closing all connections to the database
|
|
// This is done by stopping the node and restarting it with the new DB
|
|
logout()
|
|
|
|
// Replacing the old database files with the new ones, ignoring the wal and shm errors
|
|
replaceCleanup, err := replaceDBFile(dbPath, tmpDbPath)
|
|
if replaceCleanup != nil {
|
|
defer replaceCleanup()
|
|
}
|
|
|
|
if err != nil {
|
|
// Restore the old account
|
|
_ = b.reEncryptKeyStoreDir(newPassword, password)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) changeWalletDBPassword(account *multiaccounts.Account, logout func(), password string, newPassword string) error {
|
|
tmpDbPath, cleanup, err := b.createTempDBFile("wallet.db")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
dbPath, err := b.getWalletDBPath(account.KeyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Exporting database to a temporary file with a new password
|
|
err = sqlite.ExportDB(dbPath, password, account.KDFIterations, tmpDbPath, newPassword, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Replacing the old database with the new one requires closing all connections to the database
|
|
// This is done by stopping the node and restarting it with the new DB
|
|
logout()
|
|
|
|
// Replacing the old database files with the new ones, ignoring the wal and shm errors
|
|
replaceCleanup, err := replaceDBFile(dbPath, tmpDbPath)
|
|
if replaceCleanup != nil {
|
|
defer replaceCleanup()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (b *GethStatusBackend) createTempDBFile(pattern string) (tmpDbPath string, cleanup func(), err error) {
|
|
if len(b.rootDataDir) == 0 {
|
|
err = errors.New("root datadir wasn't provided")
|
|
return
|
|
}
|
|
rootDataDir := b.rootDataDir
|
|
//On iOS, the rootDataDir value does not contain a trailing slash.
|
|
//This is causing an incorrectly formatted temporary file path to be generated, leading to an "operation not permitted" error.
|
|
//e.g. value of rootDataDir is `/var/mobile/.../12906D5A-E831-49E9-BBE7-5FFE8E805D8A/Library`,
|
|
//the file path generated is something like `/var/mobile/.../12906D5A-E831-49E9-BBE7-5FFE8E805D8A/123-v4.db`
|
|
//which removed `Library` from the path.
|
|
if !strings.HasSuffix(rootDataDir, "/") {
|
|
rootDataDir += "/"
|
|
}
|
|
file, err := os.CreateTemp(filepath.Dir(rootDataDir), "*-"+pattern)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = file.Close()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
tmpDbPath = file.Name()
|
|
cleanup = func() {
|
|
filePath := file.Name()
|
|
_ = os.Remove(filePath)
|
|
_ = os.Remove(filePath + "-wal")
|
|
_ = os.Remove(filePath + "-shm")
|
|
_ = os.Remove(filePath + "-journal")
|
|
}
|
|
return
|
|
}
|
|
|
|
func replaceDBFile(dbPath string, newDBPath string) (cleanup func(), err error) {
|
|
err = os.Rename(newDBPath, dbPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cleanup = func() {
|
|
_ = os.Remove(dbPath + "-wal")
|
|
_ = os.Remove(dbPath + "-shm")
|
|
_ = os.Rename(newDBPath+"-wal", dbPath+"-wal")
|
|
_ = os.Rename(newDBPath+"-shm", dbPath+"-shm")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (b *GethStatusBackend) ConvertToKeycardAccount(account multiaccounts.Account, s settings.Settings, keycardUID string, password string, newPassword string) error {
|
|
messenger := b.Messenger()
|
|
if messenger == nil {
|
|
return errors.New("cannot resolve messenger instance")
|
|
}
|
|
|
|
err := b.multiaccountsDB.UpdateAccountKeycardPairing(account.KeyUID, account.KeycardPairing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.ensureDBsOpened(account, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accountDB, err := accounts.NewDB(b.appDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keypair, err := accountDB.GetKeypairByKeyUID(account.KeyUID)
|
|
if err != nil {
|
|
if err == accounts.ErrDbKeypairNotFound {
|
|
return errors.New("cannot convert an unknown keypair")
|
|
}
|
|
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.SaveSettingField(settings.ProfileMigrationNeeded, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// This check is added due to mobile app cause it doesn't support a Keycard features as desktop app.
|
|
// We should remove the following line once mobile and desktop app align.
|
|
if len(keycardUID) > 0 {
|
|
displayName, err := accountDB.DisplayName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
position, err := accountDB.GetPositionForNextNewKeycard()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
kc := accounts.Keycard{
|
|
KeycardUID: keycardUID,
|
|
KeycardName: displayName,
|
|
KeycardLocked: false,
|
|
KeyUID: account.KeyUID,
|
|
Position: position,
|
|
}
|
|
|
|
for _, acc := range keypair.Accounts {
|
|
kc.AccountsAddresses = append(kc.AccountsAddresses, acc.Address)
|
|
}
|
|
err = messenger.SaveOrUpdateKeycard(context.Background(), &kc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
masterAddress, err := accountDB.GetMasterAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
eip1581Address, err := accountDB.GetEIP1581Address()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
walletRootAddress, err := accountDB.GetWalletRootAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.closeDBs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.ChangeDatabasePassword(account.KeyUID, password, newPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We need to delete all accounts for the Keycard which is being added
|
|
for _, acc := range keypair.Accounts {
|
|
err = b.accountManager.DeleteAccount(acc.Address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = b.accountManager.DeleteAccount(masterAddress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.accountManager.DeleteAccount(eip1581Address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.accountManager.DeleteAccount(walletRootAddress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) RestoreAccountAndLogin(request *requests.RestoreAccount) (*multiaccounts.Account, error) {
|
|
|
|
if err := request.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
account, settings, nodeConfig, subAccounts, err := b.generateOrImportAccount(request.Mnemonic, 0, request.FetchBackup, &request.CreateAccount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = b.StartNodeWithAccountAndInitialConfig(*account, request.Password, *settings, nodeConfig, subAccounts)
|
|
if err != nil {
|
|
b.log.Error("start node", err)
|
|
return nil, err
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error) {
|
|
accountGenerator := b.accountManager.AccountsGenerator()
|
|
|
|
info, err := accountGenerator.ImportMnemonic(mnemonic, "")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return info.KeyUID, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizationColorClock uint64, fetchBackup bool, request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, *settings.Settings, *params.NodeConfig, []*accounts.Account, error) {
|
|
info, err := b.generateAccountInfo(mnemonic)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
keyStoreDir, err := b.initKeyStoreDirWithAccount(request.BackupDisabledDataDir, info.KeyUID)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
account, info, err := b.generateAccount(*info, customizationColorClock, request)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
derivedAddresses, err := b.getDerivedAddresses(info.ID)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
settings, err := b.prepareSettings(*info, derivedAddresses, request)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
processBackedupMessages := mnemonic != "" && fetchBackup
|
|
nodeConfig, err := b.prepareConfig(processBackedupMessages, account.KeyUID, keyStoreDir, request, opts...)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
subAccounts, err := b.prepareSubAccounts(mnemonic, account.KeyUID, derivedAddresses, request)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
return account, settings, nodeConfig, subAccounts, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) initKeyStoreDirWithAccount(backupDisabledDataDir, keyUID string) (string, error) {
|
|
b.UpdateRootDataDir(backupDisabledDataDir)
|
|
keystoreDir := keystoreRelativePath
|
|
userKeyStoreDir := filepath.Join(keystoreDir, keyUID)
|
|
// Initialize keystore dir with account
|
|
return userKeyStoreDir, b.accountManager.InitKeystore(filepath.Join(b.rootDataDir, userKeyStoreDir))
|
|
}
|
|
|
|
func (b *GethStatusBackend) generateAccountInfo(mnemonic string) (*generator.GeneratedAccountInfo, error) {
|
|
accountGenerator := b.accountManager.AccountsGenerator()
|
|
|
|
var info generator.GeneratedAccountInfo
|
|
var err error
|
|
if mnemonic == "" {
|
|
// generate 1(n) account with default mnemonic length and no passphrase
|
|
generatedAccountInfos, err := accountGenerator.Generate(defaultMnemonicLength, 1, "")
|
|
info = generatedAccountInfos[0]
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
|
|
info, err = accountGenerator.ImportMnemonic(mnemonic, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) generateAccount(info generator.GeneratedAccountInfo, customizationColorClock uint64, request *requests.CreateAccount) (*multiaccounts.Account, *generator.GeneratedAccountInfo, error) {
|
|
err := b.OpenAccounts()
|
|
if err != nil {
|
|
b.log.Error("failed open accounts", "err", err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
accountGenerator := b.accountManager.AccountsGenerator()
|
|
|
|
_, err = accountGenerator.StoreAccount(info.ID, request.Password)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
_, err = accountGenerator.StoreDerivedAccounts(info.ID, request.Password, paths)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
account := multiaccounts.Account{
|
|
KeyUID: info.KeyUID,
|
|
Name: request.DisplayName,
|
|
CustomizationColor: multiacccommon.CustomizationColor(request.CustomizationColor),
|
|
CustomizationColorClock: customizationColorClock,
|
|
KDFIterations: request.KdfIterations,
|
|
Timestamp: time.Now().Unix(),
|
|
}
|
|
|
|
if account.KDFIterations == 0 {
|
|
account.KDFIterations = dbsetup.ReducedKDFIterationsNumber
|
|
}
|
|
|
|
if request.ImagePath != "" {
|
|
imageCropRectangle := request.ImageCropRectangle
|
|
if imageCropRectangle == nil {
|
|
// Default crop rectangle used by mobile
|
|
imageCropRectangle = &requests.ImageCropRectangle{
|
|
Ax: 0,
|
|
Ay: 0,
|
|
Bx: 1000,
|
|
By: 1000,
|
|
}
|
|
}
|
|
|
|
iis, err := images.GenerateIdentityImages(request.ImagePath,
|
|
imageCropRectangle.Ax, imageCropRectangle.Ay, imageCropRectangle.Bx, imageCropRectangle.By)
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
account.Images = iis
|
|
}
|
|
|
|
return &account, &info, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) prepareSettings(info generator.GeneratedAccountInfo, derivedAddresses map[string]generator.AccountInfo, request *requests.CreateAccount) (*settings.Settings, error) {
|
|
settings, err := defaultSettings(info, derivedAddresses, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
settings.DeviceName = request.DeviceName
|
|
settings.DisplayName = request.DisplayName
|
|
settings.PreviewPrivacy = request.PreviewPrivacy
|
|
settings.CurrentNetwork = request.CurrentNetwork
|
|
settings.TestNetworksEnabled = request.TestNetworksEnabled
|
|
|
|
// If restoring an account, we don't set the mnemonic
|
|
if info.Mnemonic == "" {
|
|
settings.Mnemonic = &info.Mnemonic
|
|
settings.OmitTransfersHistoryScan = true
|
|
// TODO(rasom): uncomment it as soon as address will be properly
|
|
// marked as shown on mobile client
|
|
//settings.MnemonicWasNotShown = true
|
|
}
|
|
|
|
return settings, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) prepareConfig(processBackedupMessages bool, installationID string, userKeyStoreDir string, request *requests.CreateAccount, opts ...params.Option) (*params.NodeConfig, error) {
|
|
nodeConfig, err := defaultNodeConfig(installationID, request, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodeConfig.ProcessBackedupMessages = processBackedupMessages
|
|
|
|
// when we set nodeConfig.KeyStoreDir, value of nodeConfig.KeyStoreDir should not contain the rootDataDir
|
|
// loadNodeConfig will add rootDataDir to nodeConfig.KeyStoreDir
|
|
nodeConfig.KeyStoreDir = userKeyStoreDir
|
|
|
|
return nodeConfig, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) prepareSubAccounts(mnemonic, keyUID string, derivedAddresses map[string]generator.AccountInfo, request *requests.CreateAccount) ([]*accounts.Account, error) {
|
|
walletDerivedAccount := derivedAddresses[pathDefaultWallet]
|
|
walletAccount := &accounts.Account{
|
|
PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey),
|
|
KeyUID: keyUID,
|
|
Address: types.HexToAddress(walletDerivedAccount.Address),
|
|
ColorID: multiacccommon.CustomizationColor(request.CustomizationColor),
|
|
Emoji: request.Emoji,
|
|
Wallet: true,
|
|
Path: pathDefaultWallet,
|
|
Name: walletAccountDefaultName,
|
|
}
|
|
|
|
if mnemonic == "" {
|
|
walletAccount.AddressWasNotShown = true
|
|
}
|
|
|
|
chatDerivedAccount := derivedAddresses[pathDefaultChat]
|
|
chatAccount := &accounts.Account{
|
|
PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey),
|
|
KeyUID: keyUID,
|
|
Address: types.HexToAddress(chatDerivedAccount.Address),
|
|
Name: request.DisplayName,
|
|
Chat: true,
|
|
Path: pathDefaultChat,
|
|
}
|
|
|
|
return []*accounts.Account{walletAccount, chatAccount}, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) getDerivedAddresses(id string) (map[string]generator.AccountInfo, error) {
|
|
accountGenerator := b.accountManager.AccountsGenerator()
|
|
derivedAddresses, err := accountGenerator.DeriveAddresses(id, paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return derivedAddresses, nil
|
|
}
|
|
|
|
// CreateAccountAndLogin creates a new account and logs in with it.
|
|
// NOTE: requests.CreateAccount is used for public, params.Option maybe used for internal usage.
|
|
func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, error) {
|
|
validation := &requests.CreateAccountValidation{
|
|
AllowEmptyDisplayName: false,
|
|
}
|
|
if err := request.Validate(validation); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
account, settings, nodeConfig, subAccounts, err := b.generateOrImportAccount("", 1, false, request, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = b.StartNodeWithAccountAndInitialConfig(*account, request.Password, *settings, nodeConfig, subAccounts)
|
|
if err != nil {
|
|
b.log.Error("start node", err)
|
|
return nil, err
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) ConvertToRegularAccount(mnemonic string, currPassword string, newPassword string) error {
|
|
messenger := b.Messenger()
|
|
if messenger == nil {
|
|
return errors.New("cannot resolve messenger instance")
|
|
}
|
|
|
|
mnemonicNoExtraSpaces := strings.Join(strings.Fields(mnemonic), " ")
|
|
accountInfo, err := b.accountManager.AccountsGenerator().ImportMnemonic(mnemonicNoExtraSpaces, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(accountInfo.KeyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.ensureDBsOpened(multiaccounts.Account{KeyUID: accountInfo.KeyUID, KDFIterations: kdfIterations}, currPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
db, err := accounts.NewDB(b.appDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
knownAccounts, err := db.GetActiveAccounts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We add these two paths, cause others will be added via `StoreAccount` function call
|
|
const pathWalletRoot = "m/44'/60'/0'/0"
|
|
const pathEIP1581 = "m/43'/60'/1581'"
|
|
var paths []string
|
|
paths = append(paths, pathWalletRoot, pathEIP1581)
|
|
for _, acc := range knownAccounts {
|
|
if accountInfo.KeyUID == acc.KeyUID {
|
|
paths = append(paths, acc.Path)
|
|
}
|
|
}
|
|
|
|
_, err = b.accountManager.AccountsGenerator().StoreAccount(accountInfo.ID, currPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = b.accountManager.AccountsGenerator().StoreDerivedAccounts(accountInfo.ID, currPassword, paths)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.multiaccountsDB.UpdateAccountKeycardPairing(accountInfo.KeyUID, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = messenger.DeleteAllKeycardsWithKeyUID(context.Background(), accountInfo.KeyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = db.SaveSettingField(settings.KeycardInstanceUID, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = db.SaveSettingField(settings.KeycardPairedOn, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = db.SaveSettingField(settings.KeycardPairing, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = db.SaveSettingField(settings.ProfileMigrationNeeded, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.closeDBs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.ChangeDatabasePassword(accountInfo.KeyUID, currPassword, newPassword)
|
|
}
|
|
|
|
func (b *GethStatusBackend) VerifyDatabasePassword(keyUID string, password string) error {
|
|
kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(keyUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !b.appDBExists(keyUID) || !b.walletDBExists(keyUID) {
|
|
return errors.New("One or more databases not created")
|
|
}
|
|
|
|
err = b.ensureDBsOpened(multiaccounts.Account{KeyUID: keyUID, KDFIterations: kdfIterations}, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.closeDBs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func enrichMultiAccountBySubAccounts(account *multiaccounts.Account, subaccs []*accounts.Account) error {
|
|
if account.ColorHash != nil && account.ColorID != 0 {
|
|
return nil
|
|
}
|
|
|
|
for i, acc := range subaccs {
|
|
subaccs[i].KeyUID = account.KeyUID
|
|
if acc.Chat {
|
|
pk := string(acc.PublicKey.Bytes())
|
|
colorHash, err := colorhash.GenerateFor(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account.ColorHash = colorHash
|
|
|
|
colorID, err := identityutils.ToColorID(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account.ColorID = colorID
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func enrichMultiAccountByPublicKey(account *multiaccounts.Account, publicKey types.HexBytes) error {
|
|
pk := string(publicKey.Bytes())
|
|
colorHash, err := colorhash.GenerateFor(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account.ColorHash = colorHash
|
|
|
|
colorID, err := identityutils.ToColorID(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account.ColorID = colorID
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey(account multiaccounts.Account, password string, settings settings.Settings, nodecfg *params.NodeConfig, subaccs []*accounts.Account, keyHex string) error {
|
|
err := enrichMultiAccountBySubAccounts(&account, subaccs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.SaveAccount(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.ensureDBsOpened(account, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.saveAccountsAndSettings(settings, nodecfg, subaccs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.StartNodeWithKey(account, password, keyHex, nodecfg)
|
|
}
|
|
|
|
// 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 {
|
|
b.log.Info("node config", "config", nodecfg)
|
|
|
|
err := enrichMultiAccountBySubAccounts(&account, subaccs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.SaveAccount(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.ensureDBsOpened(account, password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.saveAccountsAndSettings(settings, nodecfg, subaccs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.StartNodeWithAccount(account, password, nodecfg)
|
|
}
|
|
|
|
// TODO: change in `saveAccountsAndSettings` function param `subaccs []*accounts.Account` parameter to `profileKeypair *accounts.Keypair` parameter
|
|
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
|
|
}
|
|
|
|
// In case of setting up new account either way (creating new, importing seed phrase, keycard account...) we should not
|
|
// back up any data after login, as it was the case before, that's the reason why we're setting last backup time to the time
|
|
// when an account was created.
|
|
now := time.Now().Unix()
|
|
err = accdb.SetLastBackup(uint64(now))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keypair := &accounts.Keypair{
|
|
KeyUID: settings.KeyUID,
|
|
Name: settings.DisplayName,
|
|
Type: accounts.KeypairTypeProfile,
|
|
DerivedFrom: settings.Address.String(),
|
|
LastUsedDerivationIndex: 0,
|
|
}
|
|
|
|
// When creating a new account, the chat account should have position -1, cause it doesn't participate
|
|
// in the wallet view and default wallet account should be at position 0.
|
|
for _, acc := range subaccs {
|
|
if acc.Chat {
|
|
acc.Position = -1
|
|
}
|
|
if acc.Wallet {
|
|
acc.Position = 0
|
|
}
|
|
acc.Operable = accounts.AccountFullyOperable
|
|
keypair.Accounts = append(keypair.Accounts, acc)
|
|
}
|
|
|
|
return accdb.SaveOrUpdateKeypair(keypair)
|
|
}
|
|
|
|
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 {
|
|
// If an installationID is provided, we override it
|
|
if conf != nil && conf.ShhextConfig.InstallationID != "" {
|
|
inputNodeCfg.ShhextConfig.InstallationID = conf.ShhextConfig.InstallationID
|
|
}
|
|
|
|
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
|
|
|
|
if inputNodeCfg != nil && inputNodeCfg.RuntimeLogLevel != "" {
|
|
b.config.LogLevel = inputNodeCfg.RuntimeLogLevel
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if b.accountManager.GetManager() == nil {
|
|
err = b.accountManager.InitKeystore(config.KeyStoreDir)
|
|
if 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
|
|
}
|
|
b.accountManager.SetRPCClient(b.statusNode.RPCClient(), rpc.DefaultCallTimeout)
|
|
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
|
|
}
|
|
|
|
if b.statusNode.WalletService() != nil {
|
|
b.statusNode.WalletService().KeycardPairings().SetKeycardPairingsFile(config.KeycardPairingDataFile)
|
|
}
|
|
|
|
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
|
|
}
|
|
if !b.LocalPairingStateManager.IsPairing() {
|
|
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
|
|
}
|
|
|
|
return b.transactor.SendTransaction(sendArgs, verifiedAccount)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return b.transactor.SendTransactionWithChainID(chainID, sendArgs, verifiedAccount)
|
|
}
|
|
|
|
func (b *GethStatusBackend) SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash types.Hash, err error) {
|
|
txWithSignature, err := b.transactor.BuildTransactionWithSignature(b.transactor.NetworkID(), sendArgs, sig)
|
|
if err != nil {
|
|
return hash, err
|
|
}
|
|
|
|
return b.transactor.SendTransactionWithSignature(common.Address(sendArgs.From), sendArgs.Symbol, sendArgs.MultiTransactionID, txWithSignature)
|
|
}
|
|
|
|
// 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) {
|
|
var messenger *protocol.Messenger
|
|
s, err := parseAppState(state)
|
|
if err != nil {
|
|
log.Error("AppStateChange failed, ignoring", "error", err)
|
|
return
|
|
}
|
|
|
|
b.appState = s
|
|
|
|
if b.statusNode == nil {
|
|
log.Warn("statusNode nil, not reporting app state change")
|
|
return
|
|
}
|
|
|
|
if b.statusNode.WakuExtService() != nil {
|
|
messenger = b.statusNode.WakuExtService().Messenger()
|
|
}
|
|
|
|
if b.statusNode.WakuV2ExtService() != nil {
|
|
messenger = b.statusNode.WakuV2ExtService().Messenger()
|
|
}
|
|
|
|
if messenger == nil {
|
|
log.Warn("messenger nil, not reporting app state change")
|
|
return
|
|
}
|
|
|
|
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()
|
|
|
|
b.log.Debug("logging out")
|
|
err := b.cleanupServices()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = b.closeDBs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.AccountManager().Logout()
|
|
b.account = nil
|
|
|
|
if b.statusNode != nil {
|
|
if err := b.statusNode.Stop(); err != nil {
|
|
return err
|
|
}
|
|
b.statusNode = nil
|
|
}
|
|
|
|
if !b.LocalPairingStateManager.IsPairing() {
|
|
signal.SendNodeStopped()
|
|
}
|
|
|
|
// re-initialize the node, at some point we should better manage the lifecycle
|
|
b.initialize()
|
|
|
|
err = b.statusNode.StartMediaServerWithoutDB()
|
|
if err != nil {
|
|
b.log.Error("failed to start media server without app db", "err", err)
|
|
return err
|
|
}
|
|
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) closeDBs() error {
|
|
err := b.closeWalletDB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.closeAppDB()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (b *GethStatusBackend) closeWalletDB() error {
|
|
if b.walletDB != nil {
|
|
err := b.walletDB.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.walletDB = 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) LocalPairingStarted() error {
|
|
if b.account == nil {
|
|
return errors.New("master key account is nil in the GethStatusBackend")
|
|
}
|
|
|
|
accountDB, err := accounts.NewDB(b.appDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return accountDB.MnemonicWasShown()
|
|
}
|
|
|
|
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.walletDB, b.statusNode.HTTPServer(), b.multiaccountsDB, acc, b.accountManager, b.statusNode.RPCClient(), b.statusNode.WalletService(), b.statusNode.CommunityTokensService(), b.statusNode.WakuV2Service(), 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)
|
|
b.statusNode.EnsService().Init(messenger.SyncEnsNamesWithDispatchMessage)
|
|
b.statusNode.CommunityTokensService().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)
|
|
}
|
|
|
|
func (b *GethStatusBackend) Messenger() *protocol.Messenger {
|
|
node := b.StatusNode()
|
|
if node != nil {
|
|
accountService := node.AccountService()
|
|
if accountService != nil {
|
|
return accountService.GetMessenger()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) getAppDBPath(keyUID string) (string, error) {
|
|
if len(b.rootDataDir) == 0 {
|
|
return "", errors.New("root datadir wasn't provided")
|
|
}
|
|
|
|
return filepath.Join(b.rootDataDir, fmt.Sprintf("%s-v4.db", keyUID)), nil
|
|
}
|
|
|
|
func (b *GethStatusBackend) getWalletDBPath(keyUID string) (string, error) {
|
|
if len(b.rootDataDir) == 0 {
|
|
return "", errors.New("root datadir wasn't provided")
|
|
}
|
|
|
|
return filepath.Join(b.rootDataDir, fmt.Sprintf("%s-wallet.db", keyUID)), nil
|
|
}
|