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