package wallet import ( "bytes" "encoding/hex" "errors" "fmt" "sort" "strings" "sync" "time" "github.com/p9c/p9/pkg/qu" "github.com/davecgh/go-spew/spew" "github.com/p9c/p9/pkg/amt" "github.com/p9c/p9/pkg/btcaddr" "github.com/p9c/p9/pkg/chaincfg" "github.com/p9c/p9/pod/config" "github.com/p9c/p9/pkg/blockchain" "github.com/p9c/p9/pkg/btcjson" "github.com/p9c/p9/pkg/chainclient" "github.com/p9c/p9/pkg/chainhash" ec "github.com/p9c/p9/pkg/ecc" "github.com/p9c/p9/pkg/rpcclient" "github.com/p9c/p9/pkg/txauthor" "github.com/p9c/p9/pkg/txrules" "github.com/p9c/p9/pkg/txscript" "github.com/p9c/p9/pkg/util" "github.com/p9c/p9/pkg/util/hdkeychain" "github.com/p9c/p9/pkg/waddrmgr" "github.com/p9c/p9/pkg/walletdb" "github.com/p9c/p9/pkg/wire" "github.com/p9c/p9/pkg/wtxmgr" ) const ( // InsecurePubPassphrase is the default outer encryption passphrase used for public data (everything but private // keys). Using a non-default public passphrase can prevent an attacker without the public passphrase from // discovering all past and future wallet addresses if they gain access to the wallet database. // // NOTE: at time of writing, public encryption only applies to public data in the waddrmgr namespace. Transactions // are not yet encrypted. InsecurePubPassphrase = "" // walletDbWatchingOnlyName = "wowallet.db" recoveryBatchSize is the default number of blocks that will be scanned // successively by the recovery manager, in the event that the wallet is started in recovery mode. recoveryBatchSize = 2000 ) // ErrNotSynced describes an error where an operation cannot complete due wallet being out of sync (and perhaps // currently syncing with) the remote chain server. var ErrNotSynced = errors.New("wallet is not synchronized with the chain server") // Namespace bucket keys. var ( waddrmgrNamespaceKey = []byte("waddrmgr") wtxmgrNamespaceKey = []byte("wtxmgr") ) // Wallet is a structure containing all the components for a complete wallet. It contains the Armory-style key store // addresses and keys), type Wallet struct { publicPassphrase []byte // Data stores db walletdb.DB Manager *waddrmgr.Manager TxStore *wtxmgr.Store chainClient chainclient.Interface chainClientLock sync.Mutex chainClientSynced bool chainClientSyncMtx sync.Mutex lockedOutpoints map[wire.OutPoint]struct{} recoveryWindow uint32 // Channels for rescan processing. Requests are added and merged with any waiting requests, before being sent to // another goroutine to call the rescan RPC. rescanAddJob chan *RescanJob rescanBatch chan *rescanBatch rescanNotifications chan interface{} // From chain server rescanProgress chan *RescanProgressMsg rescanFinished chan *RescanFinishedMsg // Channel for transaction creation requests. createTxRequests chan createTxRequest // Channels for the manager locker. unlockRequests chan unlockRequest lockRequests qu.C holdUnlockRequests chan chan heldUnlock lockState chan bool changePassphrase chan changePassphraseRequest changePassphrases chan changePassphrasesRequest // Information for reorganization handling. // reorganizingLock sync.Mutex // reorganizeToHash chainhash.Hash // reorganizing bool NtfnServer *NotificationServer PodConfig *config.Config chainParams *chaincfg.Params wg sync.WaitGroup started bool quit qu.C quitMu sync.Mutex Update qu.C } // Start starts the goroutines necessary to manage a wallet. func (w *Wallet) Start() { T.Ln("starting wallet") w.quitMu.Lock() T.Ln("locked wallet quit mutex") select { case <-w.quit.Wait(): T.Ln("waiting for wallet shutdown") // Restart the wallet goroutines after shutdown finishes. w.WaitForShutdown() w.quit = qu.T() default: if w.started { // Ignore when the wallet is still running. I.Ln("wallet already started") w.quitMu.Unlock() return } w.started = true } w.quitMu.Unlock() T.Ln("wallet quit mutex unlocked") w.wg.Add(2) go w.txCreator() go w.walletLocker() } // SynchronizeRPC associates the wallet with the consensus RPC client, synchronizes the wallet with the latest changes // to the blockchain, and continuously updates the wallet through RPC notifications. // // This method is unstable and will be removed when all syncing logic is moved outside of the wallet package. func (w *Wallet) SynchronizeRPC(chainClient chainclient.Interface) { T.Ln("SynchronizeRPC") w.quitMu.Lock() select { case <-w.quit.Wait(): w.quitMu.Unlock() return default: } w.quitMu.Unlock() // TODO: Ignoring the new client when one is already set breaks callers // who are replacing the client, perhaps after a disconnect. T.Ln("locking wallet chain client mutex") w.chainClientLock.Lock() if w.chainClient != nil { T.Ln("chain client is nil, unlocking wallet chain client mutex") w.chainClientLock.Unlock() return } w.chainClient = chainClient // If the chain client is a NeutrinoClient instance, set a birthday so we don't download all the filters as we go. switch cc := chainClient.(type) { // case *chainclient.NeutrinoClient: // cc.SetStartTime(w.Manager.Birthday()) case *chainclient.BitcoindClient: cc.SetBirthday(w.Manager.Birthday()) } T.Ln("unlocking wallet chain client mutex") w.chainClientLock.Unlock() T.Ln("unlocked wallet chain client mutex") // TODO: It would be preferable to either run these goroutines separately from the wallet (use wallet mutator // functions to make changes from the RPC client) and not have to stop and restart them each time the client // disconnects and reconnets. w.wg.Add(4) go w.handleChainNotifications() go w.rescanBatchHandler() go w.rescanProgressHandler() go w.rescanRPCHandler() } // requireChainClient marks that a wallet method can only be completed when the consensus RPC server is set. This // function and all functions that call it are unstable and will need to be moved when the syncing code is moved out of // the wallet. func (w *Wallet) requireChainClient() (chainclient.Interface, error) { T.Ln("requireChainClient") w.chainClientLock.Lock() chainClient := w.chainClient w.chainClientLock.Unlock() if chainClient == nil { T.Ln("chain client is nil") return nil, errors.New("wallet->chain RPC is inactive") } return chainClient, nil } // ChainClient returns the optional consensus RPC client associated with the wallet. // // This function is unstable and will be removed once sync logic is moved out of the wallet. func (w *Wallet) ChainClient() chainclient.Interface { T.Ln("wallet acquiring connect to chain RPC") w.chainClientLock.Lock() T.Ln("chainClientLock locked", w.chainClient == nil) chainClient := w.chainClient w.chainClientLock.Unlock() T.Ln("chainClientLock unlocked") return chainClient } // quitChan atomically reads the quit channel. func (w *Wallet) quitChan() qu.C { w.quitMu.Lock() c := w.quit w.quitMu.Unlock() return c } // Stop signals all wallet goroutines to shutdown. func (w *Wallet) Stop() { // T.Ln("w", w, "w.quitMu", w.quitMu) w.quitMu.Lock() defer w.quitMu.Unlock() select { case <-w.quit.Wait(): default: w.chainClientLock.Lock() if w.chainClient != nil { w.chainClient.Stop() w.chainClient = nil } w.chainClientLock.Unlock() w.quit.Q() // return } } // ShuttingDown returns whether the wallet is currently in the process of shutting down or not. func (w *Wallet) ShuttingDown() bool { select { case <-w.quitChan().Wait(): return true default: return false } } // WaitForShutdown blocks until all wallet goroutines have finished executing. func (w *Wallet) WaitForShutdown() { T.Ln("waiting for shutdown") w.chainClientLock.Lock() T.Ln("locked", w.chainClient) if w.chainClient != nil { T.Ln("calling WaitForShutdown") w.chainClient.WaitForShutdown() } T.Ln("unlocking") w.chainClientLock.Unlock() // T.Ln("waiting on waitgroup") // w.wg.Wait() } // SynchronizingToNetwork returns whether the wallet is currently synchronizing with the Bitcoin network. func (w *Wallet) SynchronizingToNetwork() bool { // At the moment, RPC is the only synchronization method. In the future, when SPV is added, a separate check will // also be needed, or SPV could always be enabled if RPC was not explicitly specified when creating the wallet. w.chainClientSyncMtx.Lock() syncing := w.chainClient != nil w.chainClientSyncMtx.Unlock() return syncing } // ChainSynced returns whether the wallet has been attached to a chain server and synced up to the best block on the // main chain. func (w *Wallet) ChainSynced() bool { w.chainClientSyncMtx.Lock() synced := w.chainClientSynced w.chainClientSyncMtx.Unlock() return synced } // SetChainSynced marks whether the wallet is connected to and currently in sync with the latest block notified by the // chain server. // // NOTE: Due to an API limitation with rpcclient, this may return true after the client disconnected (and is attempting // a reconnect). This will be unknown until the reconnect notification is received, at which point the wallet can be // marked out of sync again until after the next rescan completes. func (w *Wallet) SetChainSynced(synced bool) { w.chainClientSyncMtx.Lock() w.chainClientSynced = synced w.chainClientSyncMtx.Unlock() } // activeData returns the currently-active receiving addresses and all unspent outputs. This is primarily intended to // provide the parameters for a rescan request. func (w *Wallet) activeData(dbtx walletdb.ReadTx) ( addrs []btcaddr.Address, unspent []wtxmgr.Credit, e error, ) { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) if e = w.Manager.ForEachActiveAddress( addrmgrNs, func(addr btcaddr.Address) (e error) { addrs = append(addrs, addr) return nil }, ); E.Chk(e) { return nil, nil, e } if unspent, e = w.TxStore.UnspentOutputs(txmgrNs); E.Chk(e) { } return addrs, unspent, e } // syncWithChain brings the wallet up to date with the current chain server connection. It creates a rescan request and // blocks until the rescan has finished. func (w *Wallet) syncWithChain() (e error) { T.Ln("syncWithChain") var chainClient chainclient.Interface chainClient, e = w.requireChainClient() if e != nil { return e } // Request notifications for transactions sending to all wallet addresses. var ( addrs []btcaddr.Address unspent []wtxmgr.Credit ) e = walletdb.View( w.db, func(dbtx walletdb.ReadTx) (e error) { addrs, unspent, e = w.activeData(dbtx) return e }, ) if e != nil { W.Ln("error starting sync", e) return e } startHeight := w.Manager.SyncedTo().Height // We'll mark this as our first sync if we don't have any unspent outputs as known by the wallet. This will allow us // to skip a full rescan at this height, and instead wait for the backend to catch up. isInitialSync := len(unspent) == 0 isRecovery := w.recoveryWindow > 0 birthday := w.Manager.Birthday() // If an initial sync is attempted, we will try and find the block stamp of the first block past our birthday. This // will be fed into the rescan to ensure we catch transactions that are sent while performing the initial sync. var birthdayStamp *waddrmgr.BlockStamp // TODO(jrick): How should this handle a synced height earlier than the chain server best block? When no addresses // have been generated for the wallet, the rescan can be skipped. // // TODO: This is only correct because activeData above returns all addresses ever created, including those that // don't need to be watched anymore. This code should be updated when this assumption is no longer true, but worst // case would result in an unnecessary rescan. if isInitialSync || isRecovery { // Find the latest checkpoint's height. This lets us catch up to at least that checkpoint, since we're // synchronizing from scratch, and lets us avoid a bunch of costly DB transactions in the case when we're using // BDB for the walletdb backend and Neutrino for the chain.Interface backend, and the chain backend starts // synchronizing at the same time as the wallet. var bestHeight int32 _, bestHeight, e = chainClient.GetBestBlock() if e != nil { return e } T.Ln("bestHeight", bestHeight) checkHeight := bestHeight if len(w.chainParams.Checkpoints) > 0 { checkHeight = w.chainParams.Checkpoints[len( w.chainParams.Checkpoints, )-1].Height } logHeight := checkHeight if bestHeight > logHeight { logHeight = bestHeight } I.F( "catching up block hashes to height %d, this will take a while", logHeight, ) // Initialize the first database transaction. var tx walletdb.ReadWriteTx tx, e = w.db.BeginReadWriteTx() if e != nil { return e } ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) // Only allocate the recoveryMgr if we are actually in recovery mode. recoveryMgr := &RecoveryManager{} if isRecovery { I.Ln( "RECOVERY MODE ENABLED -- rescanning for used addresses with recovery_window =", w.recoveryWindow, ) // Initialize the recovery manager with a default batch size of 2000. I.Ln("initialising recovery manager") recoveryMgr = NewRecoveryManager( w.recoveryWindow, recoveryBatchSize, w.chainParams, ) // In the event that this recovery is being resumed, we will need to repopulate all found addresses from the // database. For basic recovery, we will only do so for the default scopes. var scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager I.Ln("getting scope managers") scopedMgrs, e = w.defaultScopeManagers() if e != nil { return e } I.Ln("opening read bucket") txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) var credits []wtxmgr.Credit I.Ln("getting unspent outputs") credits, e = w.TxStore.UnspentOutputs(txmgrNs) if e != nil { return e } I.Ln("resurrecting the dead") e = recoveryMgr.Resurrect(ns, scopedMgrs, credits) if e != nil { return e } I.Ln("all deads now shambling") } I.Ln("startHeight", startHeight, "bestHeight", bestHeight) for height := startHeight; height <= bestHeight; height++ { I.Ln("current height", height) var hash *chainhash.Hash if hash, e = chainClient.GetBlockHash(int64(height)); E.Chk(e) { if e = tx.Rollback(); E.Chk(e) { } return e } // // If we're using the Neutrino backend, we can check if it's current or not. For other backends we'll assume // // it is current if the best height has reached the last checkpoint. // isCurrent := func(bestHeight int32) bool { // switch c := chainClient.(type) { // case *chainclient.NeutrinoClient: // return c.CS.IsCurrent() // } // return bestHeight >= checkHeight // } // If we've found the best height the backend knows about, and the backend is still synchronizing, we'll // wait. We can give it a little bit of time to synchronize further before updating the best height based on // the backend. Once we see that the backend has advanced, we can catch up to it. for height == bestHeight { // && !isCurrent(bestHeight) { I.Ln("getting best height from chain") if _, bestHeight, e = chainClient.GetBestBlock(); E.Chk(e) { if e = tx.Rollback(); E.Chk(e) { } return e } else { break } time.Sleep(time.Second) } var header *wire.BlockHeader header, e = chainClient.GetBlockHeader(hash) if e != nil { return e } // Chk to see if this header's timestamp has surpassed our birthday or if we've surpassed one previously. timestamp := header.Timestamp if timestamp.After(birthday) || birthdayStamp != nil { // If this is the first block past our birthday, record the block stamp so that we can use this as the // starting point for the rescan. This will ensure we don't miss transactions that are sent to the // wallet during an initial sync. // // NOTE: The birthday persisted by the wallet is two days before the actual wallet birthday, to deal // with potentially inaccurate header timestamps. if birthdayStamp == nil { birthdayStamp = &waddrmgr.BlockStamp{ Height: height, Hash: *hash, Timestamp: timestamp, } } // If we are in recovery mode and the check passes, we will add this block to our list of blocks to scan // for recovered addresses. if isRecovery { recoveryMgr.AddToBlockBatch( hash, height, timestamp, ) } } e = w.Manager.SetSyncedTo( ns, &waddrmgr.BlockStamp{ Hash: *hash, Height: height, Timestamp: timestamp, }, ) if e != nil { e = tx.Rollback() if e != nil { } return e } // If we are in recovery mode, attempt a recovery on blocks that have been added to the recovery manager's // block batch thus far. If block batch is empty, this will be a NOP. if isRecovery && height%recoveryBatchSize == 0 { e = w.recoverDefaultScopes( chainClient, tx, ns, recoveryMgr.BlockBatch(), recoveryMgr.State(), ) if e != nil { e = tx.Rollback() if e != nil { } return e } // Clear the batch of all processed blocks. recoveryMgr.ResetBlockBatch() } // Every 10K blocks, commit and start a new database TX. if height%10000 == 0 { e = tx.Commit() if e != nil { e = tx.Rollback() if e != nil { } return e } I.Ln( "caught up to height", height, ) tx, e = w.db.BeginReadWriteTx() if e != nil { return e } ns = tx.ReadWriteBucket(waddrmgrNamespaceKey) } } // Perform one last recovery attempt for all blocks that were not batched at the default granularity of 2000 // blocks. if isRecovery { I.Ln("isRecovery") e = w.recoverDefaultScopes( chainClient, tx, ns, recoveryMgr.BlockBatch(), recoveryMgr.State(), ) if e != nil { e = tx.Rollback() if e != nil { } return e } } // Commit (or roll back) the final database transaction. if e = tx.Commit(); E.Chk(e) { if e = tx.Rollback(); E.Chk(e) { } return e } I.Ln("done catching up block hashes") // Since we've spent some time catching up block hashes, we might have new addresses waiting for us that were // requested during initial sync. Make sure we have those before we request a rescan later on. e = walletdb.View( w.db, func(dbtx walletdb.ReadTx) (e error) { addrs, unspent, e = w.activeData(dbtx) return e }, ) if e != nil { return e } } // Compare previously-seen blocks against the chain server. If any of these blocks no longer exist, rollback all of // the missing blocks before catching up with the rescan. rollback := false rollbackStamp := w.Manager.SyncedTo() e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) for height := rollbackStamp.Height; true; height-- { hash, e := w.Manager.BlockHash(addrmgrNs, height) if e != nil { return e } chainHash, e := chainClient.GetBlockHash(int64(height)) if e != nil { return e } header, e := chainClient.GetBlockHeader(chainHash) if e != nil { return e } rollbackStamp.Hash = *chainHash rollbackStamp.Height = height rollbackStamp.Timestamp = header.Timestamp if bytes.Equal(hash[:], chainHash[:]) { break } rollback = true } if rollback { e := w.Manager.SetSyncedTo(addrmgrNs, &rollbackStamp) if e != nil { return e } // Rollback unconfirms transactions at and beyond the passed height, so add one to the new synced-to height // to prevent unconfirming txs from the synced-to block. e = w.TxStore.Rollback(txmgrNs, rollbackStamp.Height+1) if e != nil { return e } } return nil }, ) if e != nil { return e } // If a birthday stamp was found during the initial sync and the rollback causes us to revert it, update the // birthday stamp so that it points at the new tip. if birthdayStamp != nil && rollbackStamp.Height <= birthdayStamp.Height { birthdayStamp = &rollbackStamp } // Request notifications for connected and disconnected blocks. // // TODO(jrick): Either request this notification only once, or when rpcclient is modified to allow some notification // request to not automatically resent on reconnect, include the notifyblocks request as well. I am leaning towards // allowing off all rpcclient notification re-registrations, in which case the code here should be left as is. e = chainClient.NotifyBlocks() if e != nil { return e } return w.rescanWithTarget(addrs, unspent, birthdayStamp) } // defaultScopeManagers fetches the ScopedKeyManagers from the wallet using the default set of key scopes. func (w *Wallet) defaultScopeManagers() ( map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, error, ) { scopedMgrs := make(map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager) for _, scope := range waddrmgr.DefaultKeyScopes { scopedMgr, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return nil, e } scopedMgrs[scope] = scopedMgr } return scopedMgrs, nil } // recoverDefaultScopes attempts to recover any addresses belonging to any active scoped key managers known to the // wallet. Recovery of each scope's default account will be done iteratively against the same batch of blocks. // // TODO(conner): parallelize/pipeline/cache intermediate network requests func (w *Wallet) recoverDefaultScopes( chainClient chainclient.Interface, tx walletdb.ReadWriteTx, ns walletdb.ReadWriteBucket, batch []wtxmgr.BlockMeta, recoveryState *RecoveryState, ) (e error) { scopedMgrs, e := w.defaultScopeManagers() if e != nil { return e } return w.recoverScopedAddresses( chainClient, tx, ns, batch, recoveryState, scopedMgrs, ) } // recoverAccountAddresses scans a range of blocks in attempts to recover any previously used addresses for a particular // account derivation path. At a high level, the algorithm works as follows: // // 1) Ensure internal and external branch horizons are fully expanded. // // 2) Filter the entire range of blocks, stopping if a non-zero number of address are contained in a particular block. // // 3) Record all internal and external addresses found in the block. // // 4) Record any outpoints found in the block that should be watched for spends // // 5) Trim the range of blocks up to and including the one reporting the addrs. // // 6) Repeat from (1) if there are still more blocks in the range. func (w *Wallet) recoverScopedAddresses( chainClient chainclient.Interface, tx walletdb.ReadWriteTx, ns walletdb.ReadWriteBucket, batch []wtxmgr.BlockMeta, recoveryState *RecoveryState, scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, ) (e error) { // If there are no blocks in the batch, we are done. if len(batch) == 0 { return nil } I.F( "scanning %d blocks for recoverable addresses", len(batch), ) expandHorizons: for scope, scopedMgr := range scopedMgrs { scopeState := recoveryState.StateForScope(scope) e = expandScopeHorizons(ns, scopedMgr, scopeState) if e != nil { return e } } // With the internal and external horizons properly expanded, we now construct the filter blocks request. The // request includes the range of blocks we intend to scan, in addition to the scope-index -> addr map for all // internal and external branches. filterReq := newFilterBlocksRequest(batch, scopedMgrs, recoveryState) // Initiate the filter blocks request using our chain backend. If an error occurs, we are unable to proceed with the // recovery. filterResp, e := chainClient.FilterBlocks(filterReq) if e != nil { return e } // If the filter response is empty, this signals that the rest of the batch was completed, and no other addresses // were discovered. As a result, no further modifications to our recovery state are required and we can proceed to // the next batch. if filterResp == nil { return nil } // Otherwise, retrieve the block info for the block that detected a non-zero number of address matches. block := batch[filterResp.BatchIndex] // Log any non-trivial findings of addresses or outpoints. logFilterBlocksResp(block, filterResp) // Report any external or internal addresses found as a result of the appropriate branch recovery state. Adding // indexes above the last-found index of either will result in the horizons being expanded upon the next iteration. // Any found addresses are also marked used using the scoped key manager. e = extendFoundAddresses(ns, filterResp, scopedMgrs, recoveryState) if e != nil { return e } // Update the global set of watched outpoints with any that were found in the block. for outPoint, addr := range filterResp.FoundOutPoints { recoveryState.AddWatchedOutPoint(&outPoint, addr) } // Finally, record all of the relevant transactions that were returned in the filter blocks response. This ensures // that these transactions and their outputs are tracked when the final rescan is performed. for _, txn := range filterResp.RelevantTxns { txRecord, e := wtxmgr.NewTxRecordFromMsgTx( txn, filterResp.BlockMeta.Time, ) if e != nil { return e } e = w.addRelevantTx(tx, txRecord, &filterResp.BlockMeta) if e != nil { return e } } // Update the batch to indicate that we've processed all block through the one that returned found addresses. batch = batch[filterResp.BatchIndex+1:] // If this was not the last block in the batch, we will repeat the filtering process again after expanding our // horizons. if len(batch) > 0 { goto expandHorizons } return nil } // expandScopeHorizons ensures that the ScopeRecoveryState has an adequately sized look ahead for both its internal and // external branches. The keys derived here are added to the scope's recovery state, but do not affect the persistent // state of the wallet. If any invalid child keys are detected, the horizon will be properly extended such that our // lookahead always includes the proper number of valid child keys. func expandScopeHorizons( ns walletdb.ReadWriteBucket, scopedMgr *waddrmgr.ScopedKeyManager, scopeState *ScopeRecoveryState, ) (e error) { // Compute the current external horizon and the number of addresses we must derive to ensure we maintain a // sufficient recovery window for the external branch. exHorizon, exWindow := scopeState.ExternalBranch.ExtendHorizon() count, childIndex := uint32(0), exHorizon for count < exWindow { keyPath := externalKeyPath(childIndex) var ep error var addr waddrmgr.ManagedAddress addr, ep = scopedMgr.DeriveFromKeyPath(ns, keyPath) switch { case ep == hdkeychain.ErrInvalidChild: // Record the existence of an invalid child with the external branch's recovery state. This also increments // the branch's horizon so that it accounts for this skipped child index. scopeState.ExternalBranch.MarkInvalidChild(childIndex) childIndex++ continue case ep != nil: return ep } // Register the newly generated external address and child index with the external branch recovery state. scopeState.ExternalBranch.AddAddr(childIndex, addr.Address()) childIndex++ count++ } // Compute the current internal horizon and the number of addresses we must derive to ensure we maintain a // sufficient recovery window for the internal branch. inHorizon, inWindow := scopeState.InternalBranch.ExtendHorizon() count, childIndex = 0, inHorizon for count < inWindow { keyPath := internalKeyPath(childIndex) addr, e := scopedMgr.DeriveFromKeyPath(ns, keyPath) switch { case e == hdkeychain.ErrInvalidChild: // Record the existence of an invalid child with the internal branch's recovery state. This also increments // the branch's horizon so that it accounts for this skipped child index. scopeState.InternalBranch.MarkInvalidChild(childIndex) childIndex++ continue case e != nil: return e } // Register the newly generated internal address and child index with the internal branch recovery state. scopeState.InternalBranch.AddAddr(childIndex, addr.Address()) childIndex++ count++ } return nil } // externalKeyPath returns the relative external derivation path /0/0/index. func externalKeyPath(index uint32) waddrmgr.DerivationPath { return waddrmgr.DerivationPath{ Account: waddrmgr.DefaultAccountNum, Branch: waddrmgr.ExternalBranch, Index: index, } } // internalKeyPath returns the relative internal derivation path /0/1/index. func internalKeyPath(index uint32) waddrmgr.DerivationPath { return waddrmgr.DerivationPath{ Account: waddrmgr.DefaultAccountNum, Branch: waddrmgr.InternalBranch, Index: index, } } // newFilterBlocksRequest constructs FilterBlocksRequests using our current block range, scoped managers, and recovery // state. func newFilterBlocksRequest( batch []wtxmgr.BlockMeta, scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, recoveryState *RecoveryState, ) *chainclient.FilterBlocksRequest { filterReq := &chainclient.FilterBlocksRequest{ Blocks: batch, ExternalAddrs: make(map[waddrmgr.ScopedIndex]btcaddr.Address), InternalAddrs: make(map[waddrmgr.ScopedIndex]btcaddr.Address), WatchedOutPoints: recoveryState.WatchedOutPoints(), } // Populate the external and internal addresses by merging the addresses sets belong to all currently tracked // scopes. for scope := range scopedMgrs { scopeState := recoveryState.StateForScope(scope) for index, addr := range scopeState.ExternalBranch.Addrs() { scopedIndex := waddrmgr.ScopedIndex{ Scope: scope, Index: index, } filterReq.ExternalAddrs[scopedIndex] = addr } for index, addr := range scopeState.InternalBranch.Addrs() { scopedIndex := waddrmgr.ScopedIndex{ Scope: scope, Index: index, } filterReq.InternalAddrs[scopedIndex] = addr } } return filterReq } // extendFoundAddresses accepts a filter blocks response that contains addresses found on chain, and advances the state // of all relevant derivation paths to match the highest found child index for each branch. func extendFoundAddresses( ns walletdb.ReadWriteBucket, filterResp *chainclient.FilterBlocksResponse, scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, recoveryState *RecoveryState, ) (e error) { // Mark all recovered external addresses as used. This will be done only for scopes that reported a non-zero number // of external addresses in this block. for scope, indexes := range filterResp.FoundExternalAddrs { // First, report all external child indexes found for this scope. This ensures that the external last-found // index will be updated to include the maximum child index seen thus far. scopeState := recoveryState.StateForScope(scope) for index := range indexes { scopeState.ExternalBranch.ReportFound(index) } scopedMgr := scopedMgrs[scope] // Now, with all found addresses reported, derive and extend all external addresses up to and including the // current last found index for this scope. exNextUnfound := scopeState.ExternalBranch.NextUnfound() exLastFound := exNextUnfound if exLastFound > 0 { exLastFound-- } e := scopedMgr.ExtendExternalAddresses( ns, waddrmgr.DefaultAccountNum, exLastFound, ) if e != nil { return e } // Finally, with the scope's addresses extended, we mark used the external addresses that were found in the // block and belong to this scope. for index := range indexes { addr := scopeState.ExternalBranch.GetAddr(index) e := scopedMgr.MarkUsed(ns, addr) if e != nil { return e } } } // Mark all recovered internal addresses as used. This will be done only for scopes that reported a non-zero number // of internal addresses in this block. for scope, indexes := range filterResp.FoundInternalAddrs { // First, report all internal child indexes found for this scope. This ensures that the internal last-found // index will be updated to include the maximum child index seen thus far. scopeState := recoveryState.StateForScope(scope) for index := range indexes { scopeState.InternalBranch.ReportFound(index) } scopedMgr := scopedMgrs[scope] // Now, with all found addresses reported, derive and extend all internal addresses up to and including the // current last found index for this scope. inNextUnfound := scopeState.InternalBranch.NextUnfound() inLastFound := inNextUnfound if inLastFound > 0 { inLastFound-- } e := scopedMgr.ExtendInternalAddresses( ns, waddrmgr.DefaultAccountNum, inLastFound, ) if e != nil { return e } // Finally, with the scope's addresses extended, we mark used the internal addresses that were found in the // block and belong to this scope. for index := range indexes { addr := scopeState.InternalBranch.GetAddr(index) e := scopedMgr.MarkUsed(ns, addr) if e != nil { return e } } } return nil } // logFilterBlocksResp provides useful logging information when filtering succeeded in finding relevant transactions. func logFilterBlocksResp( block wtxmgr.BlockMeta, resp *chainclient.FilterBlocksResponse, ) { // Log the number of external addresses found in this block. var nFoundExternal int for _, indexes := range resp.FoundExternalAddrs { nFoundExternal += len(indexes) } if nFoundExternal > 0 { T.F( "recovered %d external addrs at height=%d hash=%v", nFoundExternal, block.Height, block.Hash, ) } // Log the number of internal addresses found in this block. var nFoundInternal int for _, indexes := range resp.FoundInternalAddrs { nFoundInternal += len(indexes) } if nFoundInternal > 0 { T.F( "recovered %d internal addrs at height=%d hash=%v", nFoundInternal, block.Height, block.Hash, ) } // Log the number of outpoints found in this block. nFoundOutPoints := len(resp.FoundOutPoints) if nFoundOutPoints > 0 { T.F( "found %d spends from watched outpoints at height=%d hash=%v", nFoundOutPoints, block.Height, block.Hash, ) } } type ( createTxRequest struct { account uint32 outputs []*wire.TxOut minconf int32 feeSatPerKB amt.Amount resp chan createTxResponse } createTxResponse struct { tx *txauthor.AuthoredTx e error } ) // txCreator is responsible for the input selection and creation of transactions. These functions are the responsibility // of this method (designed to be run as its own goroutine) since input selection must be serialized, or else it is // possible to create double spends by choosing the same inputs for multiple transactions. Along with input selection, // this method is also responsible for the signing of transactions, since we don't want to end up in a situation where // we run out of inputs as multiple transactions are being created. In this situation, it would then be possible for // both requests, rather than just one, to fail due to not enough available inputs. func (w *Wallet) txCreator() { quit := w.quitChan() out: for { select { case txr := <-w.createTxRequests: var e error var h heldUnlock h, e = w.holdUnlock() if e != nil { txr.resp <- createTxResponse{nil, e} continue } var tx *txauthor.AuthoredTx tx, e = w.txToOutputs( txr.outputs, txr.account, txr.minconf, txr.feeSatPerKB, ) h.release() txr.resp <- createTxResponse{tx, e} case <-quit.Wait(): break out } } w.wg.Done() } // CreateSimpleTx creates a new signed transaction spending unspent P2PKH outputs with at least minconf confirmations // spending to any number of address/amount pairs. Change and an appropriate transaction fee are automatically included, // if necessary. All transaction creation through this function is serialized to prevent the creation of many // transactions which spend the same outputs. func (w *Wallet) CreateSimpleTx( account uint32, outputs []*wire.TxOut, minconf int32, satPerKb amt.Amount, ) (*txauthor.AuthoredTx, error) { req := createTxRequest{ account: account, outputs: outputs, minconf: minconf, feeSatPerKB: satPerKb, resp: make(chan createTxResponse), } w.createTxRequests <- req resp := <-req.resp return resp.tx, resp.e } type ( unlockRequest struct { passphrase []byte lockAfter <-chan time.Time // nil prevents the timeout. err chan error } changePassphraseRequest struct { old, new []byte private bool err chan error } changePassphrasesRequest struct { publicOld, publicNew []byte privateOld, privateNew []byte err chan error } // heldUnlock is a tool to prevent the wallet from automatically locking after some timeout before an operation // which needed the unlocked wallet has finished. Any acquired heldUnlock *must* be released (preferably with a // defer) or the wallet will forever remain unlocked. heldUnlock qu.C ) // walletLocker manages the locked/unlocked state of a wallet. func (w *Wallet) walletLocker() { var timeout <-chan time.Time holdChan := make(heldUnlock) quit := w.quitChan() // this flips to false once the first unlock has been done, for runasservice opt which shuts down on lock // first := true var e error out: for { select { case req := <-w.unlockRequests: e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) return w.Manager.Unlock(addrmgrNs, req.passphrase) }, ) if e != nil { req.err <- e continue } timeout = req.lockAfter if timeout == nil { I.Ln("the wallet has been unlocked without a time limit") } else { I.Ln("the wallet has been temporarily unlocked") } req.err <- nil continue case req := <-w.changePassphrase: e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) return w.Manager.ChangePassphrase( addrmgrNs, req.old, req.new, req.private, &waddrmgr.DefaultScryptOptions, ) }, ) req.err <- e continue case req := <-w.changePassphrases: e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) e = w.Manager.ChangePassphrase( addrmgrNs, req.publicOld, req.publicNew, false, &waddrmgr.DefaultScryptOptions, ) if e != nil { return e } return w.Manager.ChangePassphrase( addrmgrNs, req.privateOld, req.privateNew, true, &waddrmgr.DefaultScryptOptions, ) }, ) req.err <- e continue case req := <-w.holdUnlockRequests: if w.Manager.IsLocked() { close(req) continue } req <- holdChan <-holdChan // Block until the lock is released. // If, after holding onto the unlocked wallet for some time, the timeout has expired, lock it now instead of // hoping it gets unlocked next time the top level select runs. select { case <-timeout: // Let the top level select fallthrough so the wallet is locked. default: continue } case w.lockState <- w.Manager.IsLocked(): continue case <-quit.Wait(): break out case <-w.lockRequests.Wait(): // first = false case <-timeout: // first = false } // Select statement fell through by an explicit lock or the timer expiring. Lock the manager here. timeout = nil e = w.Manager.Lock() if e != nil && !waddrmgr.IsError(e, waddrmgr.ErrLocked) { E.Ln("could not lock wallet:", e) } else { I.Ln("the wallet has been locked") } // if *w.PodConfig.RunAsService && !first { // // if we are running as a service this means shut down on lock as unlocking happens only at startup // break out // } } w.wg.Done() } // Unlock unlocks the wallet's address manager and relocks it after timeout has expired. If the wallet is already // unlocked and the new passphrase is correct, the current timeout is replaced with the new one. The wallet will be // locked if the passphrase is incorrect or any other error occurs during the unlock. func (w *Wallet) Unlock(passphrase []byte, lock <-chan time.Time) (e error) { eC := make(chan error, 1) w.unlockRequests <- unlockRequest{ passphrase: passphrase, lockAfter: lock, err: eC, } return <-eC } // Lock locks the wallet's address manager. func (w *Wallet) Lock() { w.lockRequests <- struct{}{} } // Locked returns whether the account manager for a wallet is locked. func (w *Wallet) Locked() bool { return <-w.lockState } // holdUnlock prevents the wallet from being locked. The heldUnlock object *must* be released, or the wallet will // forever remain unlocked. // // TODO: To prevent the above scenario, perhaps closures should be passed to the walletLocker goroutine and disallow // callers from explicitly handling the locking mechanism. func (w *Wallet) holdUnlock() (heldUnlock, error) { req := make(chan heldUnlock) w.holdUnlockRequests <- req hl, ok := <-req if !ok { // TODO(davec): This should be defined and exported from waddrmgr. return nil, waddrmgr.ManagerError{ ErrorCode: waddrmgr.ErrLocked, Description: "address manager is locked", } } return hl, nil } // release releases the hold on the unlocked-state of the wallet and allows the wallet to be locked again. If a lock // timeout has already expired, the wallet is locked again as soon as release is called. func (c heldUnlock) release() { c <- struct{}{} } // ChangePrivatePassphrase attempts to change the passphrase for a wallet from old to new. Changing the passphrase is // synchronized with all other address manager locking and unlocking. The lock state will be the same as it was before // the password change. func (w *Wallet) ChangePrivatePassphrase(old, new []byte) (e error) { errChan := make(chan error, 1) w.changePassphrase <- changePassphraseRequest{ old: old, new: new, private: true, err: errChan, } return <-errChan } // ChangePublicPassphrase modifies the public passphrase of the wallet. func (w *Wallet) ChangePublicPassphrase(old, new []byte) (e error) { errChan := make(chan error, 1) w.changePassphrase <- changePassphraseRequest{ old: old, new: new, private: false, err: errChan, } return <-errChan } // ChangePassphrases modifies the public and private passphrase of the wallet atomically. func (w *Wallet) ChangePassphrases( publicOld, publicNew, privateOld, privateNew []byte, ) (e error) { errChan := make(chan error, 1) w.changePassphrases <- changePassphrasesRequest{ publicOld: publicOld, publicNew: publicNew, privateOld: privateOld, privateNew: privateNew, err: errChan, } return <-errChan } // // accountUsed returns whether there are any recorded transactions spending to // // a given account. It returns true if atleast one address in the account was // // used and false if no address in the account was used. // func (w *Wallet) accountUsed(addrmgrNs walletdb.ReadWriteBucket, account uint32) (bool, error) { // var used bool // e := w.Manager.ForEachAccountAddress(addrmgrNs, account, // func(maddr waddrmgr.ManagedAddress) (e error) { // used = maddr.Used(addrmgrNs) // if used { // return waddrmgr.Break // } // return nil // }) // if e == waddrmgr.Break { // e = nil // } // return used, err // } // AccountAddresses returns the addresses for every created address for an // account. func (w *Wallet) AccountAddresses(account uint32) ( addrs []btcaddr.Address, e error, ) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) return w.Manager.ForEachAccountAddress( addrmgrNs, account, func(maddr waddrmgr.ManagedAddress) (e error) { addrs = append(addrs, maddr.Address()) return nil }, ) }, ) return } // CalculateBalance sums the amounts of all unspent transaction outputs to addresses of a wallet and returns the // balance. // // If confirmations is 0, all UTXOs, even those not present in a block (height -1), will be used to get the balance. // Otherwise, a UTXO must be in a block. If confirmations is 1 or greater, the balance will be calculated based on how // many how many blocks include a UTXO. func (w *Wallet) CalculateBalance(confirms int32) ( balance amt.Amount, e error, ) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) blk := w.Manager.SyncedTo() balance, e = w.TxStore.Balance(txmgrNs, confirms, blk.Height) return e }, ) return balance, e } // Balances records total, spendable (by policy), and immature coinbase reward balance amounts. type Balances struct { Total amt.Amount Spendable amt.Amount ImmatureReward amt.Amount } // CalculateAccountBalances sums the amounts of all unspent transaction outputs to the given account of a wallet and // returns the balance. // // This function is much slower than it needs to be since transactions outputs are not indexed by the accounts they // credit to, and all unspent transaction outputs must be iterated. func (w *Wallet) CalculateAccountBalances( account uint32, confirms int32, ) (bals Balances, e error) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) // Get current block. The block height used for calculating // the number of tx confirmations. syncBlock := w.Manager.SyncedTo() var unspent []wtxmgr.Credit unspent, e = w.TxStore.UnspentOutputs(txmgrNs) if e != nil { return e } for i := range unspent { output := &unspent[i] var outputAcct uint32 var addrs []btcaddr.Address _, addrs, _, e = txscript.ExtractPkScriptAddrs( output.PkScript, w.chainParams, ) if e == nil && len(addrs) > 0 { _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } if e != nil || outputAcct != account { continue } bals.Total += output.Amount if output.FromCoinBase && !confirmed( int32(w.chainParams.CoinbaseMaturity), output.Height, syncBlock.Height, ) { bals.ImmatureReward += output.Amount } else if confirmed(confirms, output.Height, syncBlock.Height) { bals.Spendable += output.Amount } } return nil }, ) return bals, e } // CurrentAddress gets the most recently requested Bitcoin payment address from a wallet for a particular key-chain // scope. If the address has already been used (there is at least one transaction spending to it in the blockchain or // pod mempool), the next chained address is returned. func (w *Wallet) CurrentAddress( account uint32, scope waddrmgr.KeyScope, ) (btcaddr.Address, error) { chainClient, e := w.requireChainClient() if e != nil { return nil, e } manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return nil, e } var ( addr btcaddr.Address props *waddrmgr.AccountProperties ) e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) maddr, e := manager.LastExternalAddress(addrmgrNs, account) if e != nil { // If no address exists yet, create the first external address. if waddrmgr.IsError(e, waddrmgr.ErrAddressNotFound) { addr, props, e = w.newAddress( addrmgrNs, account, scope, ) } return e } // Get next chained address if the last one has already been used. if maddr.Used(addrmgrNs) { addr, props, e = w.newAddress( addrmgrNs, account, scope, ) return e } addr = maddr.Address() return nil }, ) if e != nil { return nil, e } // If the props have been initially, then we had to create a new address to satisfy the query. Notify the rpc server // about the new address. if props != nil { e = chainClient.NotifyReceived([]btcaddr.Address{addr}) if e != nil { return nil, e } w.NtfnServer.notifyAccountProperties(props) } return addr, nil } // PubKeyForAddress looks up the associated public key for a P2PKH address. func (w *Wallet) PubKeyForAddress(a btcaddr.Address) ( pubKey *ec.PublicKey, e error, ) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) managedAddr, e := w.Manager.Address(addrmgrNs, a) if e != nil { return e } managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) if !ok { return errors.New("address does not have an associated public key") } pubKey = managedPubKeyAddr.PubKey() return nil }, ) return } // PrivKeyForAddress looks up the associated private key for a P2PKH or P2PK address. func (w *Wallet) PrivKeyForAddress(a btcaddr.Address) ( privKey *ec.PrivateKey, e error, ) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) managedAddr, e := w.Manager.Address(addrmgrNs, a) if e != nil { return e } managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) if !ok { return errors.New("address does not have an associated private key") } privKey, e = managedPubKeyAddr.PrivKey() return e }, ) return privKey, e } // HaveAddress returns whether the wallet is the owner of the address a. func (w *Wallet) HaveAddress(a btcaddr.Address) (b bool, e error) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) _, e = w.Manager.Address(addrmgrNs, a) return e }, ) if e == nil { return true, nil } if waddrmgr.IsError(e, waddrmgr.ErrAddressNotFound) { return false, nil } return false, e } // AccountOfAddress finds the account that an address is associated with. func (w *Wallet) AccountOfAddress(a btcaddr.Address) (account uint32, e error) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) _, account, e = w.Manager.AddrAccount(addrmgrNs, a) return e }, ) return account, e } // AddressInfo returns detailed information regarding a wallet address. func (w *Wallet) AddressInfo(a btcaddr.Address) ( waddrmgr.ManagedAddress, error, ) { var managedAddress waddrmgr.ManagedAddress e := walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) managedAddress, e = w.Manager.Address(addrmgrNs, a) return e }, ) return managedAddress, e } // AccountNumber returns the account number for an account name under a particular key scope. func (w *Wallet) AccountNumber( scope waddrmgr.KeyScope, accountName string, ) (account uint32, e error) { var manager *waddrmgr.ScopedKeyManager manager, e = w.Manager.FetchScopedKeyManager(scope) if e != nil { return 0, e } e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) account, e = manager.LookupAccount(addrmgrNs, accountName) return e }, ) return account, e } // AccountName returns the name of an account. func (w *Wallet) AccountName( scope waddrmgr.KeyScope, accountNumber uint32, ) (string, error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return "", e } var accountName string e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) accountName, e = manager.AccountName(addrmgrNs, accountNumber) return e }, ) return accountName, e } // AccountProperties returns the properties of an account, including address indexes and name. It first fetches the // desynced information from the address manager, then updates the indexes based on the address pools. func (w *Wallet) AccountProperties( scope waddrmgr.KeyScope, acct uint32, ) (*waddrmgr.AccountProperties, error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return nil, e } var props *waddrmgr.AccountProperties e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) props, e = manager.AccountProperties(waddrmgrNs, acct) return e }, ) return props, e } // RenameAccount sets the name for an account number to newName. func (w *Wallet) RenameAccount( scope waddrmgr.KeyScope, account uint32, newName string, ) (e error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return e } var props *waddrmgr.AccountProperties e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) e = manager.RenameAccount(addrmgrNs, account, newName) if e != nil { return e } props, e = manager.AccountProperties(addrmgrNs, account) return e }, ) if e == nil { w.NtfnServer.notifyAccountProperties(props) } return e } // const maxEmptyAccounts = 100 // NextAccount creates the next account and returns its account number. The name must be unique to the account. In order // to support automatic seed restoring, new accounts may not be created when all of the previous 100 accounts have no // transaction history (this is a deviation from the BIP0044 spec, which allows no unused account gaps). func (w *Wallet) NextAccount(scope waddrmgr.KeyScope, name string) ( uint32, error, ) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return 0, e } var ( account uint32 props *waddrmgr.AccountProperties ) e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) account, e = manager.NewAccount(addrmgrNs, name) if e != nil { return e } props, e = manager.AccountProperties(addrmgrNs, account) return e }, ) if e != nil { E.Ln( "cannot fetch new account properties for notification after"+ " account creation:", e, ) } w.NtfnServer.notifyAccountProperties(props) return account, e } // CreditCategory describes the type of wallet transaction output. The category of "sent transactions" (debits) is // always "send", and is not expressed by this type. // // TODO: This is a requirement of the RPC server and should be moved. type CreditCategory byte // These constants define the possible credit categories. const ( CreditReceive CreditCategory = iota CreditGenerate CreditImmature ) // String returns the category as a string. This string may be used as the JSON string for categories as part of // listtransactions and gettransaction RPC responses. func (c CreditCategory) String() string { switch c { case CreditReceive: return "receive" case CreditGenerate: return "generate" case CreditImmature: return "immature" default: return "unknown" } } // RecvCategory returns the category of received credit outputs from a transaction record. The passed block chain height // is used to distinguish immature from mature coinbase outputs. // // TODO: This is intended for use by the RPC server and should be moved out of this package at a later time. func RecvCategory( details *wtxmgr.TxDetails, syncHeight int32, net *chaincfg.Params, ) CreditCategory { if blockchain.IsCoinBaseTx(&details.MsgTx) { if confirmed( int32(net.CoinbaseMaturity), details.Block.Height, syncHeight, ) { return CreditGenerate } return CreditImmature } return CreditReceive } // listTransactions creates a object that may be marshalled to a response result for a listtransactions RPC. // // TODO: This should be moved to the legacyrpc package. func listTransactions( tx walletdb.ReadTx, details *wtxmgr.TxDetails, addrMgr *waddrmgr.Manager, syncHeight int32, net *chaincfg.Params, ) []btcjson.ListTransactionsResult { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) var ( blockHashStr string blockTime int64 confirmations int64 ) if details.Block.Height != -1 { blockHashStr = details.Block.Hash.String() blockTime = details.Block.Time.Unix() confirmations = int64(confirms(details.Block.Height, syncHeight)) } results := []btcjson.ListTransactionsResult{} txHashStr := details.Hash.String() received := details.Received.Unix() generated := blockchain.IsCoinBaseTx(&details.MsgTx) recvCat := RecvCategory(details, syncHeight, net).String() send := len(details.Debits) != 0 // Fee can only be determined if every input is a debit. var feeF64 float64 if len(details.Debits) == len(details.MsgTx.TxIn) { var debitTotal amt.Amount for _, deb := range details.Debits { debitTotal += deb.Amount } var outputTotal amt.Amount for _, output := range details.MsgTx.TxOut { outputTotal += amt.Amount(output.Value) } // Note: The actual fee is debitTotal - outputTotal. However, this RPC reports negative numbers for fees, so the // inverse is calculated. feeF64 = (outputTotal - debitTotal).ToDUO() } outputs: for i, output := range details.MsgTx.TxOut { // Determine if this output is a credit, and if so, determine its spentness. var isCredit bool var spentCredit bool for _, cred := range details.Credits { if cred.Index == uint32(i) { // Change outputs are ignored. if cred.Change { continue outputs } isCredit = true spentCredit = cred.Spent break } } var address string var accountName string _, addrs, _, _ := txscript.ExtractPkScriptAddrs(output.PkScript, net) if len(addrs) == 1 { addr := addrs[0] address = addr.EncodeAddress() mgr, account, e := addrMgr.AddrAccount(addrmgrNs, addrs[0]) if e == nil { accountName, e = mgr.AccountName(addrmgrNs, account) if e != nil { accountName = "" } } } amountF64 := amt.Amount(output.Value).ToDUO() blockIndex := int64(details.Block.Height) result := btcjson.ListTransactionsResult{ // Fields left zeroed: // InvolvesWatchOnly // BlockIndex // // Fields set below: // Account (only for non-"send" categories) // Category // Amount // Fee Address: address, Vout: uint32(i), Confirmations: confirmations, Generated: generated, BlockHash: blockHashStr, BlockIndex: blockIndex, BlockTime: blockTime, TxID: txHashStr, WalletConflicts: []string{}, Time: received, TimeReceived: received, } // Add a received/generated/immature result if this is a credit. If the output was spent, create a second result // under the send category with the inverse of the output amount. It is therefore possible that a single output // may be included in the results set zero, one, or two times. // // Since credits are not saved for outputs that are not controlled by this wallet, all non-credits from // transactions with debits are grouped under the send category. if send || spentCredit { result.Category = "send" result.Amount = -amountF64 result.Fee = feeF64 results = append(results, result) } if isCredit { result.Account = accountName result.Category = recvCat result.Amount = amountF64 result.Fee = 0 results = append(results, result) } } return results } // ListSinceBlock returns a slice of objects with details about transactions since the given block. If the block is -1 // then all transactions are included. This is intended to be used for listsinceblock RPC replies. func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ( txList []btcjson.ListTransactionsResult, e error, ) { txList = []btcjson.ListTransactionsResult{} e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { for _, detail := range details { jsonResults := listTransactions( tx, &detail, w.Manager, syncHeight, w.chainParams, ) txList = append(txList, jsonResults...) } return false, nil } return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) }, ) return } // ListTransactions returns a slice of objects with details about a recorded // transaction. This is intended to be used for listtransactions RPC replies. func (w *Wallet) ListTransactions(from, count int) ( txList []btcjson.ListTransactionsResult, e error, ) { // txList := []btcjson.ListTransactionsResult{} // T.Ln("ListTransactions", from, count) if e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) // Get current block. The block height used for calculating the number of tx // confirmations. syncBlock := w.Manager.SyncedTo() D.Ln("synced to", syncBlock) // Need to skip the first from transactions, and after those, only include the // next count transactions. skipped := 0 n := 0 rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { // Iterate over transactions at this height in reverse order. This does nothing // for unmined transactions, which are unsorted, but it will process mined // transactions in the reverse order they were marked mined. for i := len(details) - 1; i >= 0; i-- { if from > skipped { skipped++ continue } n++ if n > count { return true, nil } jsonResults := listTransactions( tx, &details[i], w.Manager, syncBlock.Height, w.chainParams, ) txList = append(txList, jsonResults...) if len(jsonResults) > 0 { n++ } } return false, nil } // Return newer results first by starting at mempool height and working down to // the genesis block. return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) }, ); E.Chk(e) { } return } // ListAddressTransactions returns a slice of objects with details about recorded transactions to or from any address // belonging to a set. This is intended to be used for listaddresstransactions RPC replies. func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( txList []btcjson.ListTransactionsResult, e error, ) { txList = []btcjson.ListTransactionsResult{} e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) // Get current block. The block height used for calculating the number of tx confirmations. syncBlock := w.Manager.SyncedTo() rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { loopDetails: for i := range details { detail := &details[i] for _, cred := range detail.Credits { pkScript := detail.MsgTx.TxOut[cred.Index].PkScript var addrs []btcaddr.Address _, addrs, _, e = txscript.ExtractPkScriptAddrs( pkScript, w.chainParams, ) if e != nil || len(addrs) != 1 { continue } apkh, ok := addrs[0].(*btcaddr.PubKeyHash) if !ok { continue } _, ok = pkHashes[string(apkh.ScriptAddress())] if !ok { continue } jsonResults := listTransactions( tx, detail, w.Manager, syncBlock.Height, w.chainParams, ) // if e != nil { // return false, err // } txList = append(txList, jsonResults...) continue loopDetails } } return false, nil } return w.TxStore.RangeTransactions(txmgrNs, 0, -1, rangeFn) }, ) return txList, e } // ListAllTransactions returns a slice of objects with details about a recorded transaction. This is intended to be used // for listalltransactions RPC replies. func (w *Wallet) ListAllTransactions() ( txList []btcjson.ListTransactionsResult, e error, ) { txList = []btcjson.ListTransactionsResult{} e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) // Get current block. The block height used for calculating the number of tx confirmations. syncBlock := w.Manager.SyncedTo() rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { // Iterate over transactions at this height in reverse order. This does nothing for unmined transactions, // which are unsorted, but it will process mined transactions in the reverse order they were marked mined. for i := len(details) - 1; i >= 0; i-- { jsonResults := listTransactions( tx, &details[i], w.Manager, syncBlock.Height, w.chainParams, ) txList = append(txList, jsonResults...) } return false, nil } // Return newer results first by starting at mempool height and working down to the genesis block. return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) }, ) return txList, e } // BlockIdentifier identifies a block by either a height or a hash. type BlockIdentifier struct { height int32 hash *chainhash.Hash } // NewBlockIdentifierFromHeight constructs a BlockIdentifier for a block height. func NewBlockIdentifierFromHeight(height int32) *BlockIdentifier { return &BlockIdentifier{height: height} } // NewBlockIdentifierFromHash constructs a BlockIdentifier for a block hash. func NewBlockIdentifierFromHash(hash *chainhash.Hash) *BlockIdentifier { return &BlockIdentifier{hash: hash} } // GetTransactionsResult is the result of the wallet's GetTransactions method. See GetTransactions for more details. type GetTransactionsResult struct { MinedTransactions []Block UnminedTransactions []TransactionSummary } // GetTransactions returns transaction results between a starting and ending block. BlockC in the block range may be // specified by either a height or a hash. // // Because this is a possibly lengthy operation, a cancel channel is provided to cancel the task. If this channel // unblocks, the results created thus far will be returned. // // Transaction results are organized by blocks in ascending order and unmined transactions in an unspecified order. // Mined transactions are saved in a Block structure which records properties about the block. func (w *Wallet) GetTransactions( startBlock, endBlock *BlockIdentifier, cancel qu.C, ) ( res *GetTransactionsResult, e error, ) { var start, end int32 = 0, -1 w.chainClientLock.Lock() chainClient := w.chainClient w.chainClientLock.Unlock() // TODO: Fetching block heights by their hashes is inherently racy because not all block headers are saved but when // they are for SPV the db can be queried directly without this. var startResp, endResp rpcclient.FutureGetBlockVerboseResult if startBlock != nil { if startBlock.hash == nil { start = startBlock.height } else { if chainClient == nil { return nil, errors.New("no chain server client") } switch client := chainClient.(type) { case *chainclient.RPCClient: startResp = client.GetBlockVerboseTxAsync(startBlock.hash) case *chainclient.BitcoindClient: start, e = client.GetBlockHeight(startBlock.hash) if e != nil { return nil, e } // case *chainclient.NeutrinoClient: // start, e = client.GetBlockHeight(startBlock.hash) // if e != nil { // return nil, e // } } } } if endBlock != nil { if endBlock.hash == nil { end = endBlock.height } else { if chainClient == nil { return nil, errors.New("no chain server client") } switch client := chainClient.(type) { case *chainclient.RPCClient: endResp = client.GetBlockVerboseTxAsync(endBlock.hash) // case *chainclient.NeutrinoClient: // end, e = client.GetBlockHeight(endBlock.hash) // if e != nil { // return nil, e // } } } } var resp *btcjson.GetBlockVerboseResult if startResp != nil { resp, e = startResp.Receive() if e != nil { return nil, e } start = int32(resp.Height) } if endResp != nil { resp, e = endResp.Receive() if e != nil { return nil, e } end = int32(resp.Height) } res = &GetTransactionsResult{} e = walletdb.View( w.db, func(dbtx walletdb.ReadTx) (e error) { txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { // TODO: probably should make RangeTransactions not reuse the // details backing array memory. dets := make([]wtxmgr.TxDetails, len(details)) copy(dets, details) details = dets txs := make([]TransactionSummary, 0, len(details)) for i := range details { txs = append(txs, makeTxSummary(dbtx, w, &details[i])) } if details[0].Block.Height != -1 { blockHash := details[0].Block.Hash res.MinedTransactions = append( res.MinedTransactions, Block{ Hash: &blockHash, Height: details[0].Block.Height, Timestamp: details[0].Block.Time.Unix(), Transactions: txs, }, ) } else { res.UnminedTransactions = txs } select { case <-cancel.Wait(): return true, nil default: return false, nil } } return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) }, ) return res, e } // AccountResult is a single account result for the AccountsResult type. type AccountResult struct { waddrmgr.AccountProperties TotalBalance amt.Amount } // AccountsResult is the result of the wallet's Accounts method. See that method for more details. type AccountsResult struct { Accounts []AccountResult CurrentBlockHash *chainhash.Hash CurrentBlockHeight int32 } // Accounts returns the current names, numbers, and total balances of all accounts in the wallet restricted to a // particular key scope. The current chain tip is included in the result for atomicity reasons. // // TODO(jrick): Is the chain tip really needed, since only the total balances are included? func (w *Wallet) Accounts(scope waddrmgr.KeyScope) (*AccountsResult, error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return nil, e } var ( accounts []AccountResult syncBlockHash *chainhash.Hash syncBlockHeight int32 ) e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) syncBlock := w.Manager.SyncedTo() syncBlockHash = &syncBlock.Hash syncBlockHeight = syncBlock.Height unspent, e := w.TxStore.UnspentOutputs(txmgrNs) if e != nil { return e } e = manager.ForEachAccount( addrmgrNs, func(acct uint32) (e error) { props, e := manager.AccountProperties(addrmgrNs, acct) if e != nil { return e } accounts = append( accounts, AccountResult{ AccountProperties: *props, // TotalBalance set below }, ) return nil }, ) if e != nil { return e } m := make(map[uint32]*amt.Amount) for i := range accounts { a := &accounts[i] m[a.AccountNumber] = &a.TotalBalance } for i := range unspent { output := unspent[i] var outputAcct uint32 var addrs []btcaddr.Address _, addrs, _, e = txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) if e == nil && len(addrs) > 0 { _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } if e == nil { amt, ok := m[outputAcct] if ok { *amt += output.Amount } } } return nil }, ) return &AccountsResult{ Accounts: accounts, CurrentBlockHash: syncBlockHash, CurrentBlockHeight: syncBlockHeight, }, e } // AccountBalanceResult is a single result for the Wallet.AccountBalances method. type AccountBalanceResult struct { AccountNumber uint32 AccountName string AccountBalance amt.Amount } // AccountBalances returns all accounts in the wallet and their balances. Balances are determined by excluding // transactions that have not met requiredConfs confirmations. func (w *Wallet) AccountBalances( scope waddrmgr.KeyScope, requiredConfs int32, ) ([]AccountBalanceResult, error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return nil, e } var results []AccountBalanceResult e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) syncBlock := w.Manager.SyncedTo() // Fill out all account info except for the balances. lastAcct, e := manager.LastAccount(addrmgrNs) if e != nil { return e } results = make([]AccountBalanceResult, lastAcct+2) for i := range results[:len(results)-1] { var accountName string accountName, e = manager.AccountName(addrmgrNs, uint32(i)) if e != nil { return e } results[i].AccountNumber = uint32(i) results[i].AccountName = accountName } results[len(results)-1].AccountNumber = waddrmgr.ImportedAddrAccount results[len(results)-1].AccountName = waddrmgr.ImportedAddrAccountName // Fetch all unspent outputs, and iterate over them tallying each account's balance where the output script pays // to an account address and the required number of confirmations is met. unspentOutputs, e := w.TxStore.UnspentOutputs(txmgrNs) if e != nil { return e } for i := range unspentOutputs { output := &unspentOutputs[i] if !confirmed(requiredConfs, output.Height, syncBlock.Height) { continue } if output.FromCoinBase && !confirmed( int32(w.ChainParams().CoinbaseMaturity), output.Height, syncBlock.Height, ) { continue } var addrs []btcaddr.Address _, addrs, _, e = txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) if e != nil || len(addrs) == 0 { continue } outputAcct, e := manager.AddrAccount(addrmgrNs, addrs[0]) if e != nil { continue } switch { case outputAcct == waddrmgr.ImportedAddrAccount: results[len(results)-1].AccountBalance += output.Amount case outputAcct > lastAcct: return errors.New( "waddrmgr.Manager.AddrAccount returned account " + "beyond recorded last account", ) default: results[outputAcct].AccountBalance += output.Amount } } return nil }, ) return results, e } // creditSlice satisfies the sort.Interface interface to provide sorting transaction credits from oldest to newest. // Credits with the same receive time and mined in the same block are not guaranteed to be sorted by the order they // appear in the block. Credits from the same transaction are sorted by output index. type creditSlice []wtxmgr.Credit func (s creditSlice) Len() int { return len(s) } func (s creditSlice) Less(i, j int) bool { switch { // If both credits are from the same tx, txsort by output index. case s[i].OutPoint.Hash == s[j].OutPoint.Hash: return s[i].OutPoint.Index < s[j].OutPoint.Index // If both transactions are unmined, txsort by their received date. case s[i].Height == -1 && s[j].Height == -1: return s[i].Received.Before(s[j].Received) // Unmined (newer) txs always come last. case s[i].Height == -1: return false case s[j].Height == -1: return true // If both txs are mined in different blocks, txsort by block height. default: return s[i].Height < s[j].Height } } func (s creditSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // ListUnspent returns a slice of objects representing the unspent wallet transactions fitting the given criteria. The // confirmations will be more than minconf, less than maxconf and if addresses is populated only the addresses contained // within it will be considered. If we know nothing about a transaction an empty array will be returned. func (w *Wallet) ListUnspent( minconf, maxconf int32, addresses map[string]struct{}, ) (results []*btcjson.ListUnspentResult, e error) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) syncBlock := w.Manager.SyncedTo() filter := len(addresses) != 0 unspent, e := w.TxStore.UnspentOutputs(txmgrNs) if e != nil { return e } sort.Sort(sort.Reverse(creditSlice(unspent))) defaultAccountName := "default" results = make([]*btcjson.ListUnspentResult, 0, len(unspent)) for i := range unspent { output := unspent[i] // Outputs with fewer confirmations than the minimum or more confs than the maximum are excluded. confs := confirms(output.Height, syncBlock.Height) if confs < minconf || confs > maxconf { continue } // Only mature coinbase outputs are included. if output.FromCoinBase { target := int32(w.ChainParams().CoinbaseMaturity) if !confirmed(target, output.Height, syncBlock.Height) { continue } } // Exclude locked outputs from the result set. if w.LockedOutpoint(output.OutPoint) { continue } // Lookup the associated account for the output. Use the default account name in case there is no associated // account for some reason, although this should never happen. // // This will be unnecessary once transactions and outputs are grouped under the associated account in the // db. acctName := defaultAccountName var sc txscript.ScriptClass var addrs []btcaddr.Address sc, addrs, _, e = txscript.ExtractPkScriptAddrs( output.PkScript, w.chainParams, ) if e != nil { continue } if len(addrs) > 0 { smgr, acct, e := w.Manager.AddrAccount(addrmgrNs, addrs[0]) if e == nil { s, e := smgr.AccountName(addrmgrNs, acct) if e == nil { acctName = s } } } if filter { for _, addr := range addrs { _, ok := addresses[addr.EncodeAddress()] if ok { goto include } } continue } include: // At the moment watch-only addresses are not supported, so all recorded outputs that are not multisig are // "spendable". Multisig outputs are only "spendable" if all keys are controlled by this wallet. // // TODO: Each case will need updates when watch-only addrs is added. For P2PK, P2PKH, and P2SH, the address // must be looked up and not be watching-only. For multisig, all pubkeys must belong to the manager with the // associated private key (currently it only checks whether the pubkey exists, since the private key is // required at the moment). var spendable bool scSwitch: switch sc { case txscript.PubKeyHashTy: spendable = true case txscript.PubKeyTy: spendable = true // case txscript.WitnessV0ScriptHashTy: // spendable = true // case txscript.WitnessV0PubKeyHashTy: // spendable = true case txscript.MultiSigTy: for _, a := range addrs { _, e := w.Manager.Address(addrmgrNs, a) if e == nil { continue } if waddrmgr.IsError(e, waddrmgr.ErrAddressNotFound) { break scSwitch } return e } spendable = true } result := &btcjson.ListUnspentResult{ TxID: output.OutPoint.Hash.String(), Vout: output.OutPoint.Index, Account: acctName, ScriptPubKey: hex.EncodeToString(output.PkScript), Amount: output.Amount.ToDUO(), Confirmations: int64(confs), Spendable: spendable, } // BUG: this should be a JSON array so that all addresses can be included, or removed (and the caller // extracts addresses from the pkScript). if len(addrs) > 0 { result.Address = addrs[0].EncodeAddress() } results = append(results, result) } return nil }, ) return results, e } // DumpPrivKeys returns the WIF-encoded private keys for all addresses with private keys in a wallet. func (w *Wallet) DumpPrivKeys() (privkeys []string, e error) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) // Iterate over each active address, appending the private key to privkeys. return w.Manager.ForEachActiveAddress( addrmgrNs, func(addr btcaddr.Address) (e error) { var ma waddrmgr.ManagedAddress ma, e = w.Manager.Address(addrmgrNs, addr) if e != nil { return e } // Only those addresses with keys needed. pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) if !ok { return nil } var wif *util.WIF wif, e = pka.ExportPrivKey() if e != nil { // It would be nice to zero out the array here. However, since strings in go are immutable, and we have // no control over the caller I don't think we can. :( return e } privkeys = append(privkeys, wif.String()) return nil }, ) }, ) return privkeys, e } // DumpWIFPrivateKey returns the WIF encoded private key for a single wallet address. func (w *Wallet) DumpWIFPrivateKey(addr btcaddr.Address) ( address string, e error, ) { var maddr waddrmgr.ManagedAddress e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) // Get private key from wallet if it exists. maddr, e = w.Manager.Address(waddrmgrNs, addr) return e }, ) if e != nil { return "", e } pka, ok := maddr.(waddrmgr.ManagedPubKeyAddress) if !ok { return "", fmt.Errorf("address %s is not a key type", addr) } wif, e := pka.ExportPrivKey() if e != nil { return "", e } return wif.String(), nil } // ImportPrivateKey imports a private key to the wallet and writes the new wallet to disk. func (w *Wallet) ImportPrivateKey( scope waddrmgr.KeyScope, wif *util.WIF, bs *waddrmgr.BlockStamp, rescan bool, ) (string, error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return "", e } // The starting block for the key is the genesis block unless otherwise specified. var newBirthday time.Time if bs == nil { bs = &waddrmgr.BlockStamp{ Hash: *w.chainParams.GenesisHash, Height: 0, } } else { // Only update the new birthday time from default value if we actually have timestamp info in the header. var header *wire.BlockHeader header, e = w.chainClient.GetBlockHeader(&bs.Hash) if e == nil { newBirthday = header.Timestamp } } // Attempt to import private key into wallet. var addr btcaddr.Address var props *waddrmgr.AccountProperties e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) maddr, e := manager.ImportPrivateKey(addrmgrNs, wif, bs) if e != nil { return e } addr = maddr.Address() props, e = manager.AccountProperties( addrmgrNs, waddrmgr.ImportedAddrAccount, ) if e != nil { return e } return w.Manager.SetBirthday(addrmgrNs, newBirthday) }, ) if e != nil { return "", e } // Rescan blockchain for transactions with txout scripts paying to the imported address. if rescan { job := &RescanJob{ Addrs: []btcaddr.Address{addr}, OutPoints: nil, BlockStamp: *bs, } // Submit rescan job and log when the import has completed. Do not block on finishing the rescan. The rescan // success or failure is logged elsewhere, and the channel is not required to be read, so discard the return // value. _ = w.SubmitRescan(job) } else { e := w.chainClient.NotifyReceived([]btcaddr.Address{addr}) if e != nil { return "", fmt.Errorf( "failed to subscribe for address ntfns for "+ "address %s: %s", addr.EncodeAddress(), e, ) } } addrStr := addr.EncodeAddress() I.Ln("imported payment address", addrStr) w.NtfnServer.notifyAccountProperties(props) // Return the payment address string of the imported private key. return addrStr, nil } // LockedOutpoint returns whether an outpoint has been marked as locked and should not be used as an input for created // transactions. func (w *Wallet) LockedOutpoint(op wire.OutPoint) bool { _, locked := w.lockedOutpoints[op] return locked } // LockOutpoint marks an outpoint as locked, that is, it should not be used as an input for newly created transactions. func (w *Wallet) LockOutpoint(op wire.OutPoint) { w.lockedOutpoints[op] = struct{}{} } // UnlockOutpoint marks an outpoint as unlocked, that is, it may be used as an input for newly created transactions. func (w *Wallet) UnlockOutpoint(op wire.OutPoint) { delete(w.lockedOutpoints, op) } // ResetLockedOutpoints resets the set of locked outpoints so all may be used as inputs for new transactions. func (w *Wallet) ResetLockedOutpoints() { w.lockedOutpoints = map[wire.OutPoint]struct{}{} } // LockedOutpoints returns a slice of currently locked outpoints. This is intended to be used by marshaling the result // as a JSON array for listlockunspent RPC results. func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput { locked := make([]btcjson.TransactionInput, len(w.lockedOutpoints)) i := 0 for op := range w.lockedOutpoints { locked[i] = btcjson.TransactionInput{ Txid: op.Hash.String(), Vout: op.Index, } i++ } return locked } // resendUnminedTxs iterates through all transactions that spend from wallet credits that are not known to have been // mined into a block, and attempts to send each to the chain server for relay. func (w *Wallet) resendUnminedTxs() { chainClient, e := w.requireChainClient() if e != nil { E.Ln("no chain server available to resend unmined transactions", e) return } var txs []*wire.MsgTx e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) txs, e = w.TxStore.UnminedTxs(txmgrNs) return e }, ) if e != nil { E.Ln("cannot load unmined transactions for resending:", e) return } for _, tx := range txs { resp, e := chainClient.SendRawTransaction(tx, false) if e != nil { D.F( "could not resend transaction %v: %v %s", tx.TxHash(), e, ) // We'll only stop broadcasting transactions if we detect that the output has already been fully spent, is // an orphan, or is conflicting with another transaction. // // TODO(roasbeef): SendRawTransaction needs to return concrete error types, no need for string matching switch { // The following are errors returned from pod's mempool. case strings.Contains(e.Error(), "spent"): case strings.Contains(e.Error(), "orphan"): case strings.Contains(e.Error(), "conflict"): case strings.Contains(e.Error(), "already exists"): case strings.Contains(e.Error(), "negative"): // The following errors are returned from bitcoind's // mempool. case strings.Contains(e.Error(), "Missing inputs"): case strings.Contains(e.Error(), "already in block chain"): case strings.Contains(e.Error(), "fee not met"): default: continue } // As the transaction was rejected, we'll attempt to remove the unmined transaction all together. Otherwise, // we'll keep attempting to rebroadcast this, and we may be computing our balance incorrectly if this tx // credits or debits to us. tt := tx e := walletdb.Update( w.db, func(dbTx walletdb.ReadWriteTx) (e error) { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) txRec, e := wtxmgr.NewTxRecordFromMsgTx( tt, time.Now(), ) if e != nil { return e } return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) }, ) if e != nil { W.F( "unable to remove conflicting tx %v: %v %s", tt.TxHash(), e, ) continue } I.C( func() string { return "removed conflicting tx:" + spew.Sdump(tt) + " " }, ) continue } D.Ln("resent unmined transaction", resp) } } // SortedActivePaymentAddresses returns a slice of all active payment addresses in a wallet. func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { var addrStrs []string e := walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) return w.Manager.ForEachActiveAddress( addrmgrNs, func(addr btcaddr.Address) (e error) { addrStrs = append(addrStrs, addr.EncodeAddress()) return nil }, ) }, ) if e != nil { return nil, e } sort.Strings(addrStrs) return addrStrs, nil } // NewAddress returns the next external chained address for a wallet. func (w *Wallet) NewAddress( account uint32, scope waddrmgr.KeyScope, nochain bool, ) (addr btcaddr.Address, e error) { var ( chainClient chainclient.Interface props *waddrmgr.AccountProperties ) if !nochain { chainClient, e = w.requireChainClient() if e != nil { return nil, e } } e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) addr, props, e = w.newAddress(addrmgrNs, account, scope) return e }, ) if e != nil { return nil, e } if !nochain { // Notify the rpc server about the newly created address. e = chainClient.NotifyReceived([]btcaddr.Address{addr}) if e != nil { return nil, e } w.NtfnServer.notifyAccountProperties(props) } return addr, nil } func (w *Wallet) newAddress( addrmgrNs walletdb.ReadWriteBucket, account uint32, scope waddrmgr.KeyScope, ) (btcaddr.Address, *waddrmgr.AccountProperties, error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return nil, nil, e } // Get next address from wallet. var addrs []waddrmgr.ManagedAddress if addrs, e = manager.NextExternalAddresses(addrmgrNs, account, 1); E.Chk(e) { return nil, nil, e } var props *waddrmgr.AccountProperties if props, e = manager.AccountProperties(addrmgrNs, account); E.Chk(e) { E.Ln( "cannot fetch account properties for notification after deriving next external address:", e, ) return nil, nil, e } return addrs[0].Address(), props, nil } // NewChangeAddress returns a new change address for a wallet. func (w *Wallet) NewChangeAddress( account uint32, scope waddrmgr.KeyScope, ) (btcaddr.Address, error) { chainClient, e := w.requireChainClient() if e != nil { return nil, e } var addr btcaddr.Address e = walletdb.Update( w.db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) addr, e = w.newChangeAddress(addrmgrNs, account) return e }, ) if e != nil { return nil, e } // Notify the rpc server about the newly created address. e = chainClient.NotifyReceived([]btcaddr.Address{addr}) if e != nil { return nil, e } return addr, nil } func (w *Wallet) newChangeAddress( addrmgrNs walletdb.ReadWriteBucket, account uint32, ) (btcaddr.Address, error) { // As we're making a change address, we'll fetch the type of manager that is able to make p2wkh output as they're // the most efficient. scopes := w.Manager.ScopesForExternalAddrType( waddrmgr.PubKeyHash, ) manager, e := w.Manager.FetchScopedKeyManager(scopes[0]) if e != nil { return nil, e } // Get next chained change address from wallet for account. addrs, e := manager.NextInternalAddresses(addrmgrNs, account, 1) if e != nil { return nil, e } return addrs[0].Address(), nil } // confirmed checks whether a transaction at height txHeight has met minconf confirmations for a blockchain at height // curHeight. func confirmed(minconf, txHeight, curHeight int32) bool { return confirms(txHeight, curHeight) >= minconf } // confirms returns the number of confirmations for a transaction in a block at height txHeight (or -1 for an // unconfirmed tx) given the chain height curHeight. func confirms(txHeight, curHeight int32) int32 { switch { case txHeight == -1, txHeight > curHeight: return 0 default: return curHeight - txHeight + 1 } } // AccountTotalReceivedResult is a single result for the Wallet.TotalReceivedForAccounts method. type AccountTotalReceivedResult struct { AccountNumber uint32 AccountName string TotalReceived amt.Amount LastConfirmation int32 } // TotalReceivedForAccounts iterates through a wallet's transaction history, returning the total amount of Bitcoin // received for all accounts. func (w *Wallet) TotalReceivedForAccounts( scope waddrmgr.KeyScope, minConf int32, ) ([]AccountTotalReceivedResult, error) { manager, e := w.Manager.FetchScopedKeyManager(scope) if e != nil { return nil, e } var results []AccountTotalReceivedResult e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) syncBlock := w.Manager.SyncedTo() e = manager.ForEachAccount( addrmgrNs, func(account uint32) (e error) { accountName, e := manager.AccountName(addrmgrNs, account) if e != nil { return e } results = append( results, AccountTotalReceivedResult{ AccountNumber: account, AccountName: accountName, }, ) return nil }, ) if e != nil { return e } var stopHeight int32 if minConf > 0 { stopHeight = syncBlock.Height - minConf + 1 } else { stopHeight = -1 } rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { for i := range details { detail := &details[i] for _, cred := range detail.Credits { pkScript := detail.MsgTx.TxOut[cred.Index].PkScript var outputAcct uint32 var addrs []btcaddr.Address _, addrs, _, e = txscript.ExtractPkScriptAddrs(pkScript, w.chainParams) if e == nil && len(addrs) > 0 { _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } if e == nil { acctIndex := int(outputAcct) if outputAcct == waddrmgr.ImportedAddrAccount { acctIndex = len(results) - 1 } res := &results[acctIndex] res.TotalReceived += cred.Amount res.LastConfirmation = confirms( detail.Block.Height, syncBlock.Height, ) } } } return false, nil } return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) }, ) return results, e } // TotalReceivedForAddr iterates through a wallet's transaction history, returning the total amount of bitcoins received // for a single wallet address. func (w *Wallet) TotalReceivedForAddr( addr btcaddr.Address, minConf int32, ) (amount amt.Amount, e error) { e = walletdb.View( w.db, func(tx walletdb.ReadTx) (e error) { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) syncBlock := w.Manager.SyncedTo() var ( addrStr = addr.EncodeAddress() stopHeight int32 ) if minConf > 0 { stopHeight = syncBlock.Height - minConf + 1 } else { stopHeight = -1 } rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { for i := range details { detail := &details[i] for _, cred := range detail.Credits { pkScript := detail.MsgTx.TxOut[cred.Index].PkScript var addrs []btcaddr.Address _, addrs, _, e = txscript.ExtractPkScriptAddrs( pkScript, w.chainParams, ) // An error creating addresses from the output script only indicates a non-standard script, so // ignore this credit. if e != nil { continue } for _, a := range addrs { if addrStr == a.EncodeAddress() { amount += cred.Amount break } } } } return false, nil } return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) }, ) return amount, e } // SendOutputs creates and sends payment transactions. It returns the transaction hash upon success. func (w *Wallet) SendOutputs( outputs []*wire.TxOut, account uint32, minconf int32, satPerKb amt.Amount, ) (*chainhash.Hash, error) { // Ensure the outputs to be created adhere to the network's consensus rules. for _, output := range outputs { if e := txrules.CheckOutput(output, satPerKb); E.Chk(e) { return nil, e } } // Create the transaction and broadcast it to the network. The transaction will be added to the database in order to // ensure that we continue to re-broadcast the transaction upon restarts until it has been confirmed. createdTx, e := w.CreateSimpleTx(account, outputs, minconf, satPerKb) if e != nil { return nil, e } D.S(createdTx) return w.publishTransaction(createdTx.Tx) } // SignatureError records the underlying error when validating a transaction input signature. type SignatureError struct { InputIndex uint32 Error error } // SignTransaction uses secrets of the wallet, as well as additional secrets // passed in by the caller, to create and add input signatures to a transaction. // // Transaction input script validation is used to confirm that all signatures // are valid. For any invalid input, a SignatureError is added to the returns. // The final error return is reserved for unexpected or fatal errors, such as // being unable to determine a previous output script to redeem. // // The transaction pointed to by tx is modified by this function. func (w *Wallet) SignTransaction( tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScripts map[wire.OutPoint][]byte, additionalKeysByAddress map[string]*util.WIF, p2shRedeemScriptsByAddress map[string][]byte, ) (signErrors []SignatureError, e error) { I.Ln("signing transaction") I.S(tx) e = walletdb.View( w.db, func(dbtx walletdb.ReadTx) (e error) { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) for i, txIn := range tx.TxIn { prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] if !ok { prevHash := &txIn.PreviousOutPoint.Hash prevIndex := txIn.PreviousOutPoint.Index var txDetails *wtxmgr.TxDetails txDetails, e = w.TxStore.TxDetails(txmgrNs, prevHash) if e != nil { return fmt.Errorf( "cannot query previous transaction details for %v: %v", txIn.PreviousOutPoint, e, ) } if txDetails == nil { return fmt.Errorf( "%v not found", txIn.PreviousOutPoint, ) } prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript } // Set up our callbacks that we pass to txscript so it can look up the // appropriate keys and scripts by address. getKey := txscript.KeyClosure( func(addr btcaddr.Address) (*ec.PrivateKey, bool, error) { if len(additionalKeysByAddress) != 0 { addrStr := addr.EncodeAddress() var wif *util.WIF wif, ok = additionalKeysByAddress[addrStr] if !ok { return nil, false, errors.New("no key for address") } return wif.PrivKey, wif.CompressPubKey, nil } var address waddrmgr.ManagedAddress address, e = w.Manager.Address(addrmgrNs, addr) if e != nil { return nil, false, e } var pka waddrmgr.ManagedPubKeyAddress pka, ok = address.(waddrmgr.ManagedPubKeyAddress) if !ok { return nil, false, fmt.Errorf( "address %v is not "+ "a pubkey address", address.Address().EncodeAddress(), ) } var key *ec.PrivateKey key, e = pka.PrivKey() if e != nil { return nil, false, e } return key, pka.Compressed(), nil }, ) getScript := txscript.ScriptClosure( func(addr btcaddr.Address) ([]byte, error) { // If keys were provided then we can only use the redeem scripts provided with // our inputs, too. if len(additionalKeysByAddress) != 0 { addrStr := addr.EncodeAddress() script, ok := p2shRedeemScriptsByAddress[addrStr] if !ok { return nil, errors.New("no script for address") } return script, nil } address, e := w.Manager.Address(addrmgrNs, addr) if e != nil { return nil, e } sa, ok := address.(waddrmgr.ManagedScriptAddress) if !ok { return nil, errors.New( "address is not a script" + " address", ) } return sa.Script() }, ) // SigHashSingle inputs can only be signed if there's a corresponding output. However this could be already // signed, so we always verify the output. if (hashType&txscript.SigHashSingle) != txscript.SigHashSingle || i < len(tx.TxOut) { script, e := txscript.SignTxOutput( w.ChainParams(), tx, i, prevOutScript, hashType, getKey, getScript, txIn.SignatureScript, ) // Failure to sign isn't an error, it just means that the tx isn't complete. if e != nil { signErrors = append( signErrors, SignatureError{ InputIndex: uint32(i), Error: e, }, ) continue } txIn.SignatureScript = script } // Either it was already signed or we just signed it. Find out if it is completely satisfied or still needs // more. vm, e := txscript.NewEngine( prevOutScript, tx, i, txscript.StandardVerifyFlags, nil, nil, 0, ) if e == nil { e = vm.Execute() } if e != nil { signErrors = append( signErrors, SignatureError{ InputIndex: uint32(i), Error: e, }, ) } } return nil }, ) return signErrors, e } // PublishTransaction sends the transaction to the consensus RPC server so it can be propagated to other nodes and // eventually mined. // // This function is unstable and will be removed once syncing code is moved out of the wallet. func (w *Wallet) PublishTransaction(tx *wire.MsgTx) (e error) { _, e = w.publishTransaction(tx) return e } // publishTransaction is the private version of PublishTransaction which contains the primary logic required for // publishing a transaction, updating the relevant database state, and finally possible removing the transaction from // the database (along with cleaning up all inputs used, and outputs created) if the transaction is rejected by the back // end. func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { server, e := w.requireChainClient() if e != nil { return nil, e } // As we aim for this to be general reliable transaction broadcast API, we'll write this tx to disk as an // unconfirmed transaction. This way, upon restarts, we'll always rebroadcast it, and also add it to our set of // records. txRec, e := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) if e != nil { return nil, e } e = walletdb.Update( w.db, func(dbTx walletdb.ReadWriteTx) (e error) { return w.addRelevantTx(dbTx, txRec, nil) }, ) if e != nil { return nil, e } txid, e := server.SendRawTransaction(tx, false) switch { case e == nil: return txid, nil // The following are errors returned from pod's mempool. case strings.Contains(e.Error(), "spent"): fallthrough case strings.Contains(e.Error(), "orphan"): fallthrough case strings.Contains(e.Error(), "conflict"): fallthrough // The following errors are returned from bitcoind's mempool. case strings.Contains(e.Error(), "fee not met"): fallthrough case strings.Contains(e.Error(), "Missing inputs"): fallthrough case strings.Contains(e.Error(), "already in block chain"): // If the transaction was rejected, then we'll remove it from the txstore, as otherwise, we'll attempt to // continually re-broadcast it, and the utxo state of the wallet won't be accurate. dbErr := walletdb.Update( w.db, func(dbTx walletdb.ReadWriteTx) (e error) { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) }, ) if dbErr != nil { return nil, fmt.Errorf( "unable to broadcast tx: %v, "+ "unable to remove invalid tx: %v", e, dbErr, ) } return nil, e default: return nil, e } } // ChainParams returns the network parameters for the blockchain the wallet belongs to. func (w *Wallet) ChainParams() *chaincfg.Params { return w.chainParams } // Database returns the underlying walletdb database. This method is provided in order to allow applications wrapping // btcwallet to store node-specific data with the wallet's database. func (w *Wallet) Database() walletdb.DB { return w.db } // Create creates an new wallet, writing it to an empty database. If the passed seed is non-nil, it is used. Otherwise, // a secure random seed of the recommended length is generated. func Create( db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Params, birthday time.Time, ) (e error) { // If a seed was provided, ensure that it is of valid length. Otherwise, we generate a random seed for the wallet // with the recommended seed length. if seed == nil { hdSeed, e := hdkeychain.GenerateSeed( hdkeychain.RecommendedSeedLen, ) if e != nil { return e } seed = hdSeed } if len(seed) < hdkeychain.MinSeedBytes || len(seed) > hdkeychain.MaxSeedBytes { return hdkeychain.ErrInvalidSeedLen } return walletdb.Update( db, func(tx walletdb.ReadWriteTx) (e error) { addrmgrNs, e := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) if e != nil { return e } txmgrNs, e := tx.CreateTopLevelBucket(wtxmgrNamespaceKey) if e != nil { return e } e = waddrmgr.Create( addrmgrNs, seed, pubPass, privPass, params, nil, birthday, ) if e != nil { return e } return wtxmgr.Create(txmgrNs) }, ) } // Open loads an already-created wallet from the passed database and namespaces. func Open( db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, params *chaincfg.Params, recoveryWindow uint32, podConfig *config.Config, quit qu.C, ) (*Wallet, error) { // debug.PrintStack() W.Ln("opening wallet") // , string(pubPass)) e := walletdb.View( db, func(tx walletdb.ReadTx) (e error) { waddrmgrBucket := tx.ReadBucket(waddrmgrNamespaceKey) if waddrmgrBucket == nil { return errors.New("missing address manager namespace") } wtxmgrBucket := tx.ReadBucket(wtxmgrNamespaceKey) if wtxmgrBucket == nil { return errors.New("missing transaction manager namespace") } return nil }, ) if e != nil { return nil, e } T.Ln("opened wallet") // Perform upgrades as necessary. Each upgrade is done under its own transaction, which is managed by each package // itself, so the entire DB is passed instead of passing already opened write transaction. // // This will need to change later when upgrades in one package depend on data in another (such as removing chain // synchronization from address manager). T.Ln("doing address manager upgrades") e = waddrmgr.DoUpgrades(db, waddrmgrNamespaceKey, pubPass, params, cbs) if e != nil { return nil, e } T.Ln("doing txmanager upgrades") e = wtxmgr.DoUpgrades(db, wtxmgrNamespaceKey) if e != nil { return nil, e } // Open database abstraction instances var ( addrMgr *waddrmgr.Manager txMgr *wtxmgr.Store ) T.Ln("opening wallet database abstraction instances") e = walletdb.View( db, func(tx walletdb.ReadTx) (e error) { T.Ln("reading address bucket") addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) T.Ln("reading tx bucket") txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) T.Ln("opening address manager") addrMgr, e = waddrmgr.Open(addrmgrNs, pubPass, params) if e != nil { E.Ln(e, "'"+string(pubPass)+"'") return e } T.Ln("opening transaction manager") txMgr, e = wtxmgr.Open(txmgrNs, params) T.Ln("wallet database abstraction instances opened") return e }, ) if e != nil { return nil, e } T.Ln("creating wallet state") // TODO: log balance? last sync height? w := &Wallet{ publicPassphrase: pubPass, db: db, Manager: addrMgr, TxStore: txMgr, lockedOutpoints: map[wire.OutPoint]struct{}{}, recoveryWindow: recoveryWindow, rescanAddJob: make(chan *RescanJob), rescanBatch: make(chan *rescanBatch), rescanNotifications: make(chan interface{}), rescanProgress: make(chan *RescanProgressMsg), rescanFinished: make(chan *RescanFinishedMsg), createTxRequests: make(chan createTxRequest), unlockRequests: make(chan unlockRequest), lockRequests: qu.T(), holdUnlockRequests: make(chan chan heldUnlock), lockState: make(chan bool), changePassphrase: make(chan changePassphraseRequest), changePassphrases: make(chan changePassphrasesRequest), chainParams: params, PodConfig: podConfig, quit: quit, } w.NtfnServer = newNotificationServer(w) w.TxStore.NotifyUnspent = func(hash *chainhash.Hash, index uint32) { w.NtfnServer.notifyUnspentOutput(0, hash, index) } T.Ln("wallet state created") return w, nil }