// ExportWatchingWallet returns a watching-only version of the wallet serialized // database as a base64-encoded string. func (w *Wallet) ExportWatchingWallet(pubPass string) (string, error) { tmpDir, err := ioutil.TempDir("", "btcwallet") if err != nil { return "", err } defer os.RemoveAll(tmpDir) // Create a new file and write a copy of the current database into it. woDbPath := filepath.Join(tmpDir, walletDbWatchingOnlyName) fi, err := os.OpenFile(woDbPath, os.O_CREATE|os.O_RDWR, 0600) if err != nil { return "", err } if err := w.db.Copy(fi); err != nil { fi.Close() return "", err } fi.Close() defer os.Remove(woDbPath) // Open the new database, get the address manager namespace, and open // it. woDb, err := walletdb.Open("bdb", woDbPath) if err != nil { _ = os.Remove(woDbPath) return "", err } defer woDb.Close() namespace, err := woDb.Namespace(waddrmgrNamespaceKey) if err != nil { return "", err } woMgr, err := waddrmgr.Open(namespace, []byte(pubPass), w.chainParams, nil) if err != nil { return "", err } defer woMgr.Close() // Convert the namespace to watching only if needed. if err := woMgr.ConvertToWatchingOnly(); err != nil { // Only return the error is it's not because it's already // watching-only. When it is already watching-only, the code // just falls through to the export below. if !waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly) { return "", err } } // Export the watching only wallet's serialized data. woWallet := *w woWallet.db = woDb woWallet.Manager = woMgr return woWallet.exportBase64() }
// TestIsError tests the IsError func. func TestIsError(t *testing.T) { tests := []struct { err error code waddrmgr.ErrorCode exp bool }{ { err: waddrmgr.ManagerError{ ErrorCode: waddrmgr.ErrDatabase, }, code: waddrmgr.ErrDatabase, exp: true, }, { // package should never return *ManagerError err: &waddrmgr.ManagerError{ ErrorCode: waddrmgr.ErrDatabase, }, code: waddrmgr.ErrDatabase, exp: false, }, { err: waddrmgr.ManagerError{ ErrorCode: waddrmgr.ErrCrypto, }, code: waddrmgr.ErrDatabase, exp: false, }, { err: errors.New("not a ManagerError"), code: waddrmgr.ErrDatabase, exp: false, }, } for i, test := range tests { got := waddrmgr.IsError(test.err, test.code) if got != test.exp { t.Errorf("Test %d: got %v expected %v", i, got, test.exp) } } }
// CurrentAddress gets the most recently requested Bitcoin payment address // from a wallet. If the address has already been used (there is at least // one transaction spending to it in the blockchain or btcd mempool), the next // chained address is returned. func (w *Wallet) CurrentAddress(account uint32) (btcutil.Address, error) { addr, err := w.Manager.LastExternalAddress(account) if err != nil { // If no address exists yet, create the first external address if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { return w.NewAddress(account) } return nil, err } // Get next chained address if the last one has already been used. used, err := addr.Used() if err != nil { return nil, err } if used { return w.NewAddress(account) } return addr.Address(), nil }
// addTestTx adds an output spendable by our test wallet, marked as included in // 'block'. func addTestTx(w *LightningWallet, rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error { err := w.TxStore.InsertTx(rec, block) if err != nil { return err } // Check every output to determine whether it is controlled by a wallet // key. If so, mark the output as a credit. for i, output := range rec.MsgTx.TxOut { _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, ActiveNetParams) if err != nil { // Non-standard outputs are skipped. continue } for _, addr := range addrs { ma, err := w.Manager.Address(addr) if err == nil { err = w.TxStore.AddCredit(rec, block, uint32(i), ma.Internal()) if err != nil { return err } err = w.Manager.MarkUsed(addr) if err != nil { return err } continue } // Missing addresses are skipped. Other errors should // be propagated. if !waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { return err } } } return nil }
func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error { // TODO: The transaction store and address manager need to be updated // together, but each operate under different namespaces and are changed // under new transactions. This is not error safe as we lose // transaction semantics. // // I'm unsure of the best way to solve this. Some possible solutions // and drawbacks: // // 1. Open write transactions here and pass the handle to every // waddrmr and wtxmgr method. This complicates the caller code // everywhere, however. // // 2. Move the wtxmgr namespace into the waddrmgr namespace, likely // under its own bucket. This entire function can then be moved // into the waddrmgr package, which updates the nested wtxmgr. // This removes some of separation between the components. // // 3. Use multiple wtxmgrs, one for each account, nested in the // waddrmgr namespace. This still provides some sort of logical // separation (transaction handling remains in another package, and // is simply used by waddrmgr), but may result in duplicate // transactions being saved if they are relevant to multiple // accounts. // // 4. Store wtxmgr-related details under the waddrmgr namespace, but // solve the drawback of #3 by splitting wtxmgr to save entire // transaction records globally for all accounts, with // credit/debit/balance tracking per account. Each account would // also save the relevant transaction hashes and block incidence so // the full transaction can be loaded from the waddrmgr // transactions bucket. This currently seems like the best // solution. // At the moment all notified transactions are assumed to actually be // relevant. This assumption will not hold true when SPV support is // added, but until then, simply insert the transaction because there // should either be one or more relevant inputs or outputs. err := w.TxStore.InsertTx(rec, block) if err != nil { return err } // Check every output to determine whether it is controlled by a wallet // key. If so, mark the output as a credit. for i, output := range rec.MsgTx.TxOut { _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) if err != nil { // Non-standard outputs are skipped. continue } for _, addr := range addrs { ma, err := w.Manager.Address(addr) if err == nil { // TODO: Credits should be added with the // account they belong to, so wtxmgr is able to // track per-account balances. err = w.TxStore.AddCredit(rec, block, uint32(i), ma.Internal()) if err != nil { return err } err = w.Manager.MarkUsed(addr) if err != nil { return err } log.Debugf("Marked address %v used", addr) continue } // Missing addresses are skipped. Other errors should // be propagated. if !waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { return err } } } // TODO: Notify connected clients of the added transaction. bs, err := w.chainSvr.BlockStamp() if err == nil { w.notifyBalances(bs.Height) } return nil }
// 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{}) ([]*btcjson.ListUnspentResult, error) { syncBlock := w.Manager.SyncedTo() filter := len(addresses) != 0 unspent, err := w.TxStore.UnspentOutputs() if err != nil { return nil, err } sort.Sort(sort.Reverse(creditSlice(unspent))) defaultAccountName, err := w.Manager.AccountName(waddrmgr.DefaultAccountNum) if err != nil { return nil, err } 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 { const target = blockchain.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 sc, addrs, _, err := txscript.ExtractPkScriptAddrs( output.PkScript, w.chainParams) if err != nil { continue } if len(addrs) > 0 { acct, err := w.Manager.AddrAccount(addrs[0]) if err == nil { s, err := w.Manager.AccountName(acct) if err == 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.ScriptHashTy: spendable = true case txscript.MultiSigTy: for _, a := range addrs { _, err := w.Manager.Address(a) if err == nil { continue } if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { break scSwitch } return nil, err } spendable = true } result := &btcjson.ListUnspentResult{ TxID: output.OutPoint.Hash.String(), Vout: output.OutPoint.Index, Account: acctName, ScriptPubKey: hex.EncodeToString(output.PkScript), Amount: output.Amount.ToBTC(), 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 results, nil }