github.com/decred/dcrlnd@v0.7.6/lnwallet/dcrwallet/loader/loader.go (about)

     1  // Copyright (c) 2015-2018 The btcsuite developers
     2  // Copyright (c) 2017-2019 The Decred developers
     3  // Use of this source code is governed by an ISC
     4  // license that can be found in the LICENSE file.
     5  
     6  package loader
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"sync"
    14  	"time"
    15  
    16  	"decred.org/dcrwallet/v4/errors"
    17  	"decred.org/dcrwallet/v4/wallet"
    18  	_ "decred.org/dcrwallet/v4/wallet/drivers/bdb" // driver loaded during init
    19  	"decred.org/dcrwallet/v4/wallet/txrules"
    20  	"decred.org/dcrwallet/v4/wallet/udb"
    21  	"github.com/decred/dcrd/chaincfg/v3"
    22  	"github.com/decred/dcrd/dcrutil/v4"
    23  	"github.com/decred/dcrd/hdkeychain/v3"
    24  	"github.com/decred/dcrlnd/kvdb"
    25  )
    26  
    27  const (
    28  	walletDbName = "wallet.db"
    29  	driver       = "bdb"
    30  )
    31  
    32  // LoaderOption is similar to the upstream btcwallet.LoaderOption type, but not
    33  // yet used.
    34  type LoaderOption func(l *Loader)
    35  
    36  func LoaderWithExternalWalletDB(backend kvdb.Backend) LoaderOption {
    37  	panic("unimplemented")
    38  }
    39  
    40  func LoaderWithLocalWalletDB(dbDirPath string, noSyncFreeList bool, dbTimeout time.Duration) LoaderOption {
    41  	return func(l *Loader) {
    42  		if dbDirPath != l.dbDirPath {
    43  			panic(fmt.Sprintf("wrong usage: %s != %s", dbDirPath, l.dbDirPath))
    44  		}
    45  	}
    46  }
    47  
    48  // Loader implements the creating of new and opening of existing wallets, while
    49  // providing a callback system for other subsystems to handle the loading of a
    50  // wallet.  This is primarely intended for use by the RPC servers, to enable
    51  // methods and services which require the wallet when the wallet is loaded by
    52  // another subsystem.
    53  //
    54  // Loader is safe for concurrent access.
    55  type Loader struct {
    56  	callbacks   []func(*wallet.Wallet)
    57  	chainParams *chaincfg.Params
    58  	dbDirPath   string
    59  	wallet      *wallet.Wallet
    60  	db          wallet.DB
    61  
    62  	gapLimit                uint32
    63  	accountGapLimit         int
    64  	disableCoinTypeUpgrades bool
    65  	allowHighFees           bool
    66  	relayFee                dcrutil.Amount
    67  
    68  	mu sync.Mutex
    69  }
    70  
    71  // NewLoader constructs a Loader.
    72  func NewLoader(chainParams *chaincfg.Params, dbDirPath string, gapLimit uint32) *Loader {
    73  
    74  	allowHighFees := false
    75  	relayFee := txrules.DefaultRelayFeePerKb
    76  	accountGapLimit := wallet.DefaultAccountGapLimit
    77  	disableCoinTypeUpgrades := defaultDisableCoinTypeUpgrades
    78  
    79  	return &Loader{
    80  		chainParams:             chainParams,
    81  		dbDirPath:               dbDirPath,
    82  		gapLimit:                gapLimit,
    83  		accountGapLimit:         accountGapLimit,
    84  		disableCoinTypeUpgrades: disableCoinTypeUpgrades,
    85  		allowHighFees:           allowHighFees,
    86  		relayFee:                relayFee,
    87  	}
    88  }
    89  
    90  // onLoaded executes each added callback and prevents loader from loading any
    91  // additional wallets.  Requires mutex to be locked.
    92  func (l *Loader) onLoaded(w *wallet.Wallet, db wallet.DB) {
    93  	for _, fn := range l.callbacks {
    94  		fn(w)
    95  	}
    96  
    97  	l.wallet = w
    98  	l.db = db
    99  	l.callbacks = nil // not needed anymore
   100  }
   101  
   102  // RunAfterLoad adds a function to be executed when the loader creates or opens
   103  // a wallet.  Functions are executed in a single goroutine in the order they are
   104  // added.
   105  func (l *Loader) RunAfterLoad(fn func(*wallet.Wallet)) {
   106  	l.mu.Lock()
   107  	if l.wallet != nil {
   108  		w := l.wallet
   109  		l.mu.Unlock()
   110  		fn(w)
   111  	} else {
   112  		l.callbacks = append(l.callbacks, fn)
   113  		l.mu.Unlock()
   114  	}
   115  }
   116  
   117  // CreateWatchingOnlyWallet creates a new watch-only wallet using the provided
   118  // extended public key and public passphrase.
   119  func (l *Loader) CreateWatchingOnlyWallet(ctx context.Context, extendedPubKey string, pubPass []byte) (w *wallet.Wallet, err error) {
   120  	const op errors.Op = "loader.CreateWatchingOnlyWallet"
   121  
   122  	defer l.mu.Unlock()
   123  	l.mu.Lock()
   124  
   125  	if l.wallet != nil {
   126  		return nil, errors.E(op, errors.Exist, "wallet already loaded")
   127  	}
   128  
   129  	// Ensure that the network directory exists.
   130  	if fi, err := os.Stat(l.dbDirPath); err != nil {
   131  		if os.IsNotExist(err) {
   132  			// Attempt data directory creation
   133  			if err = os.MkdirAll(l.dbDirPath, 0700); err != nil {
   134  				return nil, errors.E(op, err)
   135  			}
   136  		} else {
   137  			return nil, errors.E(op, err)
   138  		}
   139  	} else {
   140  		if !fi.IsDir() {
   141  			return nil, errors.E(op, errors.Invalid, errors.Errorf("%q is not a directory", l.dbDirPath))
   142  		}
   143  	}
   144  
   145  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   146  	exists, err := fileExists(dbPath)
   147  	if err != nil {
   148  		return nil, errors.E(op, err)
   149  	}
   150  	if exists {
   151  		return nil, errors.E(op, errors.Exist, "wallet already exists")
   152  	}
   153  
   154  	// At this point it is asserted that there is no existing database file, and
   155  	// deleting anything won't destroy a wallet in use.  Defer a function that
   156  	// attempts to remove any written database file if this function errors.
   157  	defer func() {
   158  		if err != nil {
   159  			_ = os.Remove(dbPath)
   160  		}
   161  	}()
   162  
   163  	// Create the wallet database backed by bbolt db.
   164  	err = os.MkdirAll(l.dbDirPath, 0700)
   165  	if err != nil {
   166  		return nil, errors.E(op, err)
   167  	}
   168  	db, err := wallet.CreateDB(driver, dbPath)
   169  	if err != nil {
   170  		return nil, errors.E(op, err)
   171  	}
   172  
   173  	// Initialize the watch-only database for the wallet before opening.
   174  	err = wallet.CreateWatchOnly(ctx, db, extendedPubKey, pubPass, l.chainParams)
   175  	if err != nil {
   176  		return nil, errors.E(op, err)
   177  	}
   178  
   179  	// Open the watch-only wallet.
   180  	cfg := &wallet.Config{
   181  		DB:                      db,
   182  		PubPassphrase:           pubPass,
   183  		GapLimit:                l.gapLimit,
   184  		AccountGapLimit:         l.accountGapLimit,
   185  		DisableCoinTypeUpgrades: l.disableCoinTypeUpgrades,
   186  		AllowHighFees:           l.allowHighFees,
   187  		RelayFee:                l.relayFee,
   188  		Params:                  l.chainParams,
   189  	}
   190  	w, err = wallet.Open(ctx, cfg)
   191  	if err != nil {
   192  		return nil, errors.E(op, err)
   193  	}
   194  
   195  	l.onLoaded(w, db)
   196  	return w, nil
   197  }
   198  
   199  func (l *Loader) CreateNewWalletExtendedKey(ctx context.Context,
   200  	pubPassphrase, privPassphrase []byte,
   201  	xpriv *hdkeychain.ExtendedKey) (w *wallet.Wallet, err error) {
   202  
   203  	panic("boo")
   204  }
   205  
   206  // CreateNewWallet creates a new wallet using the provided public and private
   207  // passphrases.  The seed is optional.  If non-nil, addresses are derived from
   208  // this seed.  If nil, a secure random seed is generated.
   209  func (l *Loader) CreateNewWallet(ctx context.Context, pubPassphrase,
   210  	privPassphrase, seed []byte, birthday time.Time) (w *wallet.Wallet, err error) {
   211  	const op errors.Op = "loader.CreateNewWallet"
   212  
   213  	defer l.mu.Unlock()
   214  	l.mu.Lock()
   215  
   216  	if l.wallet != nil {
   217  		return nil, errors.E(op, errors.Exist, "wallet already opened")
   218  	}
   219  
   220  	// Ensure that the network directory exists.
   221  	if fi, err := os.Stat(l.dbDirPath); err != nil {
   222  		if os.IsNotExist(err) {
   223  			// Attempt data directory creation
   224  			if err = os.MkdirAll(l.dbDirPath, 0700); err != nil {
   225  				return nil, errors.E(op, err)
   226  			}
   227  		} else {
   228  			return nil, errors.E(op, err)
   229  		}
   230  	} else {
   231  		if !fi.IsDir() {
   232  			return nil, errors.E(op, errors.Errorf("%q is not a directory", l.dbDirPath))
   233  		}
   234  	}
   235  
   236  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   237  	exists, err := fileExists(dbPath)
   238  	if err != nil {
   239  		return nil, errors.E(op, err)
   240  	}
   241  	if exists {
   242  		return nil, errors.E(op, errors.Exist, "wallet DB exists")
   243  	}
   244  
   245  	// At this point it is asserted that there is no existing database file, and
   246  	// deleting anything won't destroy a wallet in use.  Defer a function that
   247  	// attempts to remove any written database file if this function errors.
   248  	defer func() {
   249  		if err != nil {
   250  			_ = os.Remove(dbPath)
   251  		}
   252  	}()
   253  
   254  	// Create the wallet database backed by bbolt db.
   255  	err = os.MkdirAll(l.dbDirPath, 0700)
   256  	if err != nil {
   257  		return nil, errors.E(op, err)
   258  	}
   259  	db, err := wallet.CreateDB(driver, dbPath)
   260  	if err != nil {
   261  		return nil, errors.E(op, err)
   262  	}
   263  
   264  	// Initialize the newly created database for the wallet before opening.
   265  	err = wallet.Create(ctx, db, pubPassphrase, privPassphrase, seed, l.chainParams)
   266  	if err != nil {
   267  		return nil, errors.E(op, err)
   268  	}
   269  
   270  	// Open the newly-created wallet.
   271  	cfg := &wallet.Config{
   272  		DB:                      db,
   273  		PubPassphrase:           pubPassphrase,
   274  		GapLimit:                l.gapLimit,
   275  		AccountGapLimit:         l.accountGapLimit,
   276  		DisableCoinTypeUpgrades: l.disableCoinTypeUpgrades,
   277  		AllowHighFees:           l.allowHighFees,
   278  		RelayFee:                l.relayFee,
   279  		Params:                  l.chainParams,
   280  	}
   281  	w, err = wallet.Open(ctx, cfg)
   282  	if err != nil {
   283  		return nil, errors.E(op, err)
   284  	}
   285  
   286  	bs := &udb.BirthdayState{
   287  		SetFromTime: true,
   288  		Time:        birthday,
   289  	}
   290  	if err := w.SetBirthState(ctx, bs); err != nil {
   291  		return nil, errors.E(op, err)
   292  	}
   293  
   294  	l.onLoaded(w, db)
   295  	return w, nil
   296  }
   297  
   298  // OpenExistingWallet opens the wallet from the loader's wallet database path
   299  // and the public passphrase.  If the loader is being called by a context where
   300  // standard input prompts may be used during wallet upgrades, setting
   301  // canConsolePrompt will enable these prompts.
   302  func (l *Loader) OpenExistingWallet(ctx context.Context, pubPassphrase []byte) (w *wallet.Wallet, rerr error) {
   303  	const op errors.Op = "loader.OpenExistingWallet"
   304  
   305  	defer l.mu.Unlock()
   306  	l.mu.Lock()
   307  
   308  	if l.wallet != nil {
   309  		return nil, errors.E(op, errors.Exist, "wallet already opened")
   310  	}
   311  
   312  	// Open the database using the bboltdb backend.
   313  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   314  	l.mu.Unlock()
   315  	db, err := wallet.OpenDB(driver, dbPath)
   316  	l.mu.Lock()
   317  
   318  	if err != nil {
   319  		log.Errorf("Failed to open database: %v", err)
   320  		return nil, errors.E(op, err)
   321  	}
   322  	// If this function does not return to completion the database must be
   323  	// closed.  Otherwise, because the database is locked on opens, any
   324  	// other attempts to open the wallet will hang, and there is no way to
   325  	// recover since this db handle would be leaked.
   326  	defer func() {
   327  		if rerr != nil {
   328  			db.Close()
   329  		}
   330  	}()
   331  
   332  	cfg := &wallet.Config{
   333  		DB:                      db,
   334  		PubPassphrase:           pubPassphrase,
   335  		GapLimit:                l.gapLimit,
   336  		AccountGapLimit:         l.accountGapLimit,
   337  		DisableCoinTypeUpgrades: l.disableCoinTypeUpgrades,
   338  		AllowHighFees:           l.allowHighFees,
   339  		RelayFee:                l.relayFee,
   340  		Params:                  l.chainParams,
   341  	}
   342  	w, err = wallet.Open(ctx, cfg)
   343  	if err != nil {
   344  		return nil, errors.E(op, err)
   345  	}
   346  
   347  	l.onLoaded(w, db)
   348  	return w, nil
   349  }
   350  
   351  // DbDirPath returns the Loader's database directory path
   352  func (l *Loader) DbDirPath() string {
   353  	return l.dbDirPath
   354  }
   355  
   356  // WalletExists returns whether a file exists at the loader's database path.
   357  // This may return an error for unexpected I/O failures.
   358  func (l *Loader) WalletExists() (bool, error) {
   359  	const op errors.Op = "loader.WalletExists"
   360  	dbPath := filepath.Join(l.dbDirPath, walletDbName)
   361  	exists, err := fileExists(dbPath)
   362  	if err != nil {
   363  		return false, errors.E(op, err)
   364  	}
   365  	return exists, nil
   366  }
   367  
   368  // LoadedWallet returns the loaded wallet, if any, and a bool for whether the
   369  // wallet has been loaded or not.  If true, the wallet pointer should be safe to
   370  // dereference.
   371  func (l *Loader) LoadedWallet() (*wallet.Wallet, bool) {
   372  	l.mu.Lock()
   373  	w := l.wallet
   374  	l.mu.Unlock()
   375  	return w, w != nil
   376  }
   377  
   378  // UnloadWallet stops the loaded wallet, if any, and closes the wallet database.
   379  // Returns with errors.Invalid if the wallet has not been loaded with
   380  // CreateNewWallet or LoadExistingWallet.  The Loader may be reused if this
   381  // function returns without error.
   382  func (l *Loader) UnloadWallet() error {
   383  	const op errors.Op = "loader.UnloadWallet"
   384  
   385  	defer l.mu.Unlock()
   386  	l.mu.Lock()
   387  
   388  	if l.wallet == nil {
   389  		return errors.E(op, errors.Invalid, "wallet is unopened")
   390  	}
   391  
   392  	err := l.db.Close()
   393  	if err != nil {
   394  		return errors.E(op, err)
   395  	}
   396  
   397  	l.wallet = nil
   398  	l.db = nil
   399  	return nil
   400  }
   401  
   402  // NetworkBackend returns the associated wallet network backend, if any, and a
   403  // bool describing whether a non-nil network backend was set.
   404  func (l *Loader) NetworkBackend() (n wallet.NetworkBackend, ok bool) {
   405  	l.mu.Lock()
   406  	if l.wallet != nil {
   407  		n, _ = l.wallet.NetworkBackend()
   408  	}
   409  	l.mu.Unlock()
   410  	return n, n != nil
   411  }
   412  
   413  func fileExists(filePath string) (bool, error) {
   414  	_, err := os.Stat(filePath)
   415  	if err != nil {
   416  		if os.IsNotExist(err) {
   417  			return false, nil
   418  		}
   419  		return false, err
   420  	}
   421  	return true, nil
   422  }