loader.go raw

   1  package wallet
   2  
   3  import (
   4  	"errors"
   5  	"os"
   6  	"path/filepath"
   7  	"sync"
   8  	"time"
   9  
  10  	"github.com/p9c/p9/pkg/qu"
  11  
  12  	"github.com/p9c/p9/pkg/chaincfg"
  13  	"github.com/p9c/p9/pkg/util/prompt"
  14  	"github.com/p9c/p9/pkg/waddrmgr"
  15  	"github.com/p9c/p9/pkg/walletdb"
  16  	"github.com/p9c/p9/pod/config"
  17  )
  18  
  19  // Loader implements the creating of new and opening of existing wallets, while providing a callback system for other
  20  // subsystems to handle the loading of a wallet. This is primarily intended for use by the RPC servers, to enable
  21  // methods and services which require the wallet when the wallet is loaded by another subsystem.
  22  //
  23  // Loader is safe for concurrent access.
  24  type Loader struct {
  25  	Callbacks      []func(*Wallet)
  26  	ChainParams    *chaincfg.Params
  27  	DDDirPath      string
  28  	RecoveryWindow uint32
  29  	Wallet         *Wallet
  30  	Loaded         bool
  31  	DB             walletdb.DB
  32  	Mutex          sync.Mutex
  33  }
  34  
  35  const ()
  36  
  37  var (
  38  	// ErrExists describes the error condition of attempting to create a new wallet when one exists already.
  39  	ErrExists = errors.New("wallet already exists")
  40  	// ErrLoaded describes the error condition of attempting to load or create a wallet when the loader has already done
  41  	// so.
  42  	ErrLoaded = errors.New("wallet already loaded")
  43  	// ErrNotLoaded describes the error condition of attempting to close a loaded wallet when a wallet has not been
  44  	// loaded.
  45  	ErrNotLoaded = errors.New("wallet is not loaded")
  46  	errNoConsole = errors.New("db upgrade requires console access for additional input")
  47  )
  48  
  49  // CreateNewWallet creates a new wallet using the provided public and private passphrases. The seed is optional. If
  50  // non-nil, addresses are derived from this seed. If nil, a secure random seed is generated.
  51  func (ld *Loader) CreateNewWallet(
  52  	pubPassphrase, privPassphrase, seed []byte,
  53  	bday time.Time,
  54  	noStart bool,
  55  	podConfig *config.Config,
  56  	quit qu.C,
  57  ) (w *Wallet, e error) {
  58  	ld.Mutex.Lock()
  59  	defer ld.Mutex.Unlock()
  60  	if ld.Loaded {
  61  		return nil, ErrLoaded
  62  	}
  63  	// dbPath := filepath.Join(ld.DDDirPath, WalletDbName)
  64  	var exists bool
  65  	if exists, e = fileExists(ld.DDDirPath); E.Chk(e) {
  66  		return nil, e
  67  	}
  68  	if exists {
  69  		return nil, errors.New("Wallet ERROR: " + ld.DDDirPath + " already exists")
  70  	}
  71  	// Create the wallet database backed by bolt db.
  72  	p := filepath.Dir(ld.DDDirPath)
  73  	if e = os.MkdirAll(p, 0700); E.Chk(e) {
  74  		return nil, e
  75  	}
  76  	var db walletdb.DB
  77  	if db, e = walletdb.Create("bdb", ld.DDDirPath); E.Chk(e) {
  78  		return nil, e
  79  	}
  80  	// Initialize the newly created database for the wallet before opening.
  81  	if e = Create(db, pubPassphrase, privPassphrase, seed, ld.ChainParams,
  82  		bday); E.Chk(e) {
  83  		return nil, e
  84  	}
  85  	// Open the newly-created wallet.
  86  	if w, e = Open(db, pubPassphrase, nil, ld.ChainParams, ld.RecoveryWindow,
  87  		podConfig, quit); E.Chk(e) {
  88  		return nil, e
  89  	}
  90  	if !noStart {
  91  		w.Start()
  92  		ld.onLoaded(db)
  93  	} else {
  94  		if e = w.db.Close(); E.Chk(e) {
  95  		}
  96  	}
  97  	return w, nil
  98  }
  99  
 100  // LoadedWallet returns the loaded wallet, if any, and a bool for whether the wallet has been loaded or not. If true,
 101  // the wallet pointer should be safe to dereference.
 102  func (ld *Loader) LoadedWallet() (*Wallet, bool) {
 103  	ld.Mutex.Lock()
 104  	w := ld.Wallet
 105  	ld.Mutex.Unlock()
 106  	return w, w != nil
 107  }
 108  
 109  // OpenExistingWallet opens the wallet from the loader's wallet database path and the public passphrase. If the loader
 110  // is being called by a context where standard input prompts may be used during wallet upgrades, setting
 111  // canConsolePrompt will enables these prompts.
 112  func (ld *Loader) OpenExistingWallet(
 113  	pubPassphrase []byte,
 114  	canConsolePrompt bool,
 115  	podConfig *config.Config,
 116  	quit qu.C,
 117  ) (w *Wallet, e error) {
 118  	defer ld.Mutex.Unlock()
 119  	ld.Mutex.Lock()
 120  	I.Ln("opening existing wallet", ld.DDDirPath)
 121  	if ld.Loaded {
 122  		I.Ln("already loaded wallet")
 123  		return nil, ErrLoaded
 124  	}
 125  	// Ensure that the network directory exists.
 126  	if e = checkCreateDir(filepath.Dir(ld.DDDirPath)); E.Chk(e) {
 127  		E.Ln("cannot create directory", ld.DDDirPath)
 128  		return nil, e
 129  	}
 130  	D.Ln("directory exists")
 131  	// Open the database using the boltdb backend.
 132  	dbPath := ld.DDDirPath
 133  	I.Ln("opening database", dbPath)
 134  	var db walletdb.DB
 135  	if db, e = walletdb.Open("bdb", dbPath); E.Chk(e) {
 136  		E.Ln("failed to open database '", ld.DDDirPath)
 137  		return nil, e
 138  	}
 139  	I.Ln("opened wallet database")
 140  	var cbs *waddrmgr.OpenCallbacks
 141  	if canConsolePrompt {
 142  		cbs = &waddrmgr.OpenCallbacks{
 143  			ObtainSeed:        prompt.ProvideSeed,
 144  			ObtainPrivatePass: prompt.ProvidePrivPassphrase,
 145  		}
 146  	} else {
 147  		cbs = &waddrmgr.OpenCallbacks{
 148  			ObtainSeed:        noConsole,
 149  			ObtainPrivatePass: noConsole,
 150  		}
 151  	}
 152  	D.Ln("opening wallet '" + string(pubPassphrase) + "'")
 153  	if w, e = Open(
 154  		db,
 155  		pubPassphrase,
 156  		cbs,
 157  		ld.ChainParams,
 158  		ld.RecoveryWindow,
 159  		podConfig,
 160  		quit,
 161  	); E.Chk(e) {
 162  		E.Ln("failed to open wallet", e)
 163  		// If opening the wallet fails (e.g. because of wrong passphrase), we must close the backing database to allow
 164  		// future calls to walletdb.Open().
 165  		if e = db.Close(); E.Chk(e) {
 166  			W.Ln("error closing database:", e)
 167  		}
 168  		return nil, e
 169  	}
 170  	ld.Wallet = w
 171  	D.Ln("starting wallet", w != nil)
 172  	w.Start()
 173  	D.Ln("waiting for load", db != nil)
 174  	ld.onLoaded(db)
 175  	D.Ln("wallet opened successfully", w != nil)
 176  	return w, nil
 177  }
 178  
 179  // RunAfterLoad adds a function to be executed when the loader creates or opens a wallet. Functions are executed in a
 180  // single goroutine in the order they are added.
 181  func (ld *Loader) RunAfterLoad(fn func(*Wallet)) {
 182  	ld.Mutex.Lock()
 183  	if ld.Loaded {
 184  		// w := ld.Wallet
 185  		ld.Mutex.Unlock()
 186  		fn(ld.Wallet)
 187  	} else {
 188  		ld.Callbacks = append(ld.Callbacks, fn)
 189  		ld.Mutex.Unlock()
 190  	}
 191  }
 192  
 193  // UnloadWallet stops the loaded wallet, if any, and closes the wallet database. This returns ErrNotLoaded if the wallet
 194  // has not been loaded with CreateNewWallet or LoadExistingWallet. The Loader may be reused if this function returns
 195  // without error.
 196  func (ld *Loader) UnloadWallet() (e error) {
 197  	F.Ln("unloading wallet")
 198  	defer ld.Mutex.Unlock()
 199  	ld.Mutex.Lock()
 200  	if ld.Wallet == nil {
 201  		D.Ln("wallet not loaded")
 202  		return ErrNotLoaded
 203  	}
 204  	F.Ln("wallet stopping")
 205  	ld.Wallet.Stop()
 206  	F.Ln("waiting for wallet shutdown")
 207  	ld.Wallet.WaitForShutdown()
 208  	if ld.DB == nil {
 209  		D.Ln("there was no database")
 210  		return ErrNotLoaded
 211  	}
 212  	F.Ln("wallet stopped")
 213  	e = ld.DB.Close()
 214  	if e != nil {
 215  		D.Ln("error closing database", e)
 216  		return e
 217  	}
 218  	F.Ln("database closed")
 219  	ld.Loaded = false
 220  	ld.DB = nil
 221  	return nil
 222  }
 223  
 224  // WalletExists returns whether a file exists at the loader's database path. This may return an error for unexpected I/O
 225  // failures.
 226  func (ld *Loader) WalletExists() (bool, error) {
 227  	return fileExists(ld.DDDirPath)
 228  }
 229  
 230  // onLoaded executes each added callback and prevents loader from loading any additional wallets. Requires mutex to be
 231  // locked.
 232  func (ld *Loader) onLoaded(db walletdb.DB) {
 233  	D.Ln("wallet loader callbacks running ", ld.Wallet != nil)
 234  	for i, fn := range ld.Callbacks {
 235  		D.Ln("running wallet loader callback", i)
 236  		fn(ld.Wallet)
 237  	}
 238  	D.Ln("wallet loader callbacks finished")
 239  	ld.Loaded = true
 240  	ld.DB = db
 241  	ld.Callbacks = nil // not needed anymore
 242  }
 243  
 244  // NewLoader constructs a Loader with an optional recovery window. If the recovery window is non-zero, the wallet will
 245  // attempt to recovery addresses starting from the last SyncedTo height.
 246  func NewLoader(
 247  	chainParams *chaincfg.Params, dbDirPath string, recoveryWindow uint32,
 248  ) *Loader {
 249  	l := &Loader{
 250  		ChainParams:    chainParams,
 251  		DDDirPath:      dbDirPath,
 252  		RecoveryWindow: recoveryWindow,
 253  	}
 254  	return l
 255  }
 256  func fileExists(filePath string) (bool, error) {
 257  	_, e := os.Stat(filePath)
 258  	if e != nil {
 259  		if os.IsNotExist(e) {
 260  			return false, nil
 261  		}
 262  		return false, e
 263  	}
 264  	return true, nil
 265  }
 266  func noConsole() ([]byte, error) {
 267  	return nil, errNoConsole
 268  }
 269