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