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 }