Merge pull request #733 from wpaulino/watch-only-account-utils

waddrmgr+wallet: support transaction creation and signing for watch-only accounts
This commit is contained in:
Olaoluwa Osuntokun 2021-03-29 16:32:42 -07:00 committed by GitHub
commit e0607006dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 568 additions and 221 deletions

View file

@ -11,4 +11,3 @@ script:
- make lint - make lint
- make unit-race - make unit-race
- make unit-cover - make unit-cover
- make goveralls

View file

@ -1,12 +1,10 @@
PKG := github.com/btcsuite/btcwallet PKG := github.com/btcsuite/btcwallet
GOVERALLS_PKG := github.com/mattn/goveralls
LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
GOACC_PKG := github.com/ory/go-acc GOACC_PKG := github.com/ory/go-acc
GOIMPORTS_PKG := golang.org/x/tools/cmd/goimports GOIMPORTS_PKG := golang.org/x/tools/cmd/goimports
GO_BIN := ${GOPATH}/bin GO_BIN := ${GOPATH}/bin
GOVERALLS_BIN := $(GO_BIN)/goveralls
LINT_BIN := $(GO_BIN)/golangci-lint LINT_BIN := $(GO_BIN)/golangci-lint
GOACC_BIN := $(GO_BIN)/go-acc GOACC_BIN := $(GO_BIN)/go-acc
@ -49,10 +47,6 @@ all: build check
# DEPENDENCIES # DEPENDENCIES
# ============ # ============
$(GOVERALLS_BIN):
@$(call print, "Fetching goveralls.")
go get -u $(GOVERALLS_PKG)
$(LINT_BIN): $(LINT_BIN):
@$(call print, "Fetching linter") @$(call print, "Fetching linter")
$(DEPGET) $(LINT_PKG)@$(LINT_COMMIT) $(DEPGET) $(LINT_PKG)@$(LINT_COMMIT)
@ -97,10 +91,6 @@ unit-race:
@$(call print, "Running unit race tests.") @$(call print, "Running unit race tests.")
env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOLIST) | $(XARGS) env $(GOTEST) -race env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOLIST) | $(XARGS) env $(GOTEST) -race
goveralls: $(GOVERALLS_BIN)
@$(call print, "Sending coverage report.")
$(GOVERALLS_BIN) -coverprofile=coverage.txt -service=travis-ci
# ========= # =========
# UTILITIES # UTILITIES
# ========= # =========
@ -126,7 +116,6 @@ clean:
unit \ unit \
unit-cover \ unit-cover \
unit-race \ unit-race \
goveralls \
fmt \ fmt \
lint \ lint \
clean clean

View file

@ -22,6 +22,7 @@ import (
"github.com/btcsuite/btcwallet/netparams" "github.com/btcsuite/btcwallet/netparams"
"github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wallet/txrules" "github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/btcsuite/btcwallet/wallet/txsizes"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
) )
@ -190,14 +191,22 @@ func makeInputSource(outputs []btcjson.ListUnspentResult) txauthor.InputSource {
// makeDestinationScriptSource creates a ChangeSource which is used to receive // makeDestinationScriptSource creates a ChangeSource which is used to receive
// all correlated previous input value. A non-change address is created by this // all correlated previous input value. A non-change address is created by this
// function. // function.
func makeDestinationScriptSource(rpcClient *rpcclient.Client, accountName string) txauthor.ChangeSource { func makeDestinationScriptSource(rpcClient *rpcclient.Client, accountName string) *txauthor.ChangeSource {
return func() ([]byte, error) {
// GetNewAddress always returns a P2PKH address since it assumes
// BIP-0044.
newChangeScript := func() ([]byte, error) {
destinationAddress, err := rpcClient.GetNewAddress(accountName) destinationAddress, err := rpcClient.GetNewAddress(accountName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return txscript.PayToAddrScript(destinationAddress) return txscript.PayToAddrScript(destinationAddress)
} }
return &txauthor.ChangeSource{
ScriptSize: txsizes.P2PKHPkScriptSize,
NewScript: newChangeScript,
}
} }
func main() { func main() {

1
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 github.com/btcsuite/btcwallet/wallet/txrules v1.0.0
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0
github.com/btcsuite/btcwallet/walletdb v1.3.4 github.com/btcsuite/btcwallet/walletdb v1.3.4
github.com/btcsuite/btcwallet/wtxmgr v1.2.0 github.com/btcsuite/btcwallet/wtxmgr v1.2.0
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792

View file

@ -1293,20 +1293,14 @@ func listAllTransactions(icmd interface{}, w *wallet.Wallet) (interface{}, error
func listUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) { func listUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
cmd := icmd.(*btcjson.ListUnspentCmd) cmd := icmd.(*btcjson.ListUnspentCmd)
var addresses map[string]struct{} if cmd.Addresses != nil && len(*cmd.Addresses) > 0 {
if cmd.Addresses != nil { return nil, &btcjson.RPCError{
addresses = make(map[string]struct{}) Code: btcjson.ErrRPCInvalidParameter,
// confirm that all of them are good: Message: "Filtering by addresses has been deprecated",
for _, as := range *cmd.Addresses {
a, err := decodeAddress(as, w.ChainParams())
if err != nil {
return nil, err
}
addresses[a.EncodeAddress()] = struct{}{}
} }
} }
return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), addresses) return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), "")
} }
// lockUnspent handles the lockunspent command. // lockUnspent handles the lockunspent command.
@ -1359,13 +1353,14 @@ func makeOutputs(pairs map[string]btcutil.Amount, chainParams *chaincfg.Params)
// It returns the transaction hash in string format upon success // It returns the transaction hash in string format upon success
// All errors are returned in btcjson.RPCError format // All errors are returned in btcjson.RPCError format
func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount, func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount,
account uint32, minconf int32, feeSatPerKb btcutil.Amount) (string, error) { keyScope waddrmgr.KeyScope, account uint32, minconf int32,
feeSatPerKb btcutil.Amount) (string, error) {
outputs, err := makeOutputs(amounts, w.ChainParams()) outputs, err := makeOutputs(amounts, w.ChainParams())
if err != nil { if err != nil {
return "", err return "", err
} }
tx, err := w.SendOutputs(outputs, account, minconf, feeSatPerKb, "") tx, err := w.SendOutputs(outputs, &keyScope, account, minconf, feeSatPerKb, "")
if err != nil { if err != nil {
if err == txrules.ErrAmountNegative { if err == txrules.ErrAmountNegative {
return "", ErrNeedPositiveAmount return "", ErrNeedPositiveAmount
@ -1433,7 +1428,7 @@ func sendFrom(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient)
cmd.ToAddress: amt, cmd.ToAddress: amt,
} }
return sendPairs(w, pairs, account, minConf, return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, account, minConf,
txrules.DefaultRelayFeePerKb) txrules.DefaultRelayFeePerKb)
} }
@ -1475,7 +1470,7 @@ func sendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
pairs[k] = amt pairs[k] = amt
} }
return sendPairs(w, pairs, account, minConf, txrules.DefaultRelayFeePerKb) return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, account, minConf, txrules.DefaultRelayFeePerKb)
} }
// sendToAddress handles a sendtoaddress RPC request by creating a new // sendToAddress handles a sendtoaddress RPC request by creating a new
@ -1511,7 +1506,7 @@ func sendToAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
} }
// sendtoaddress always spends from the default account, this matches bitcoind // sendtoaddress always spends from the default account, this matches bitcoind
return sendPairs(w, pairs, waddrmgr.DefaultAccountNum, 1, return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, waddrmgr.DefaultAccountNum, 1,
txrules.DefaultRelayFeePerKb) txrules.DefaultRelayFeePerKb)
} }

View file

@ -408,7 +408,7 @@ func (s *walletServer) GetTransactions(ctx context.Context, req *pb.GetTransacti
_ = minRecentTxs _ = minRecentTxs
gtr, err := s.wallet.GetTransactions(startBlock, endBlock, ctx.Done()) gtr, err := s.wallet.GetTransactions(startBlock, endBlock, "", ctx.Done())
if err != nil { if err != nil {
return nil, translateError(err) return nil, translateError(err)
} }

View file

@ -405,6 +405,29 @@ func (m *Manager) watchOnly() bool {
return m.watchingOnly return m.watchingOnly
} }
// IsWatchOnlyAccount determines if the account with the given key scope is set
// up as watch-only.
func (m *Manager) IsWatchOnlyAccount(ns walletdb.ReadBucket, keyScope KeyScope,
account uint32) (bool, error) {
if m.WatchOnly() {
return true, nil
}
// Assume the default imported account has no private keys.
//
// TODO: Actually check whether it does.
if account == ImportedAddrAccount {
return true, nil
}
scopedMgr, err := m.FetchScopedKeyManager(keyScope)
if err != nil {
return false, err
}
return scopedMgr.IsWatchOnlyAccount(ns, account)
}
// lock performs a best try effort to remove and zero all secret keys associated // lock performs a best try effort to remove and zero all secret keys associated
// with the address manager. // with the address manager.
// //
@ -1218,7 +1241,7 @@ func (m *Manager) Unlock(ns walletdb.ReadBucket, passphrase []byte) error {
// We'll also derive any private keys that are pending due to // We'll also derive any private keys that are pending due to
// them being created while the address manager was locked. // them being created while the address manager was locked.
for _, info := range manager.deriveOnUnlock { for _, info := range manager.deriveOnUnlock {
addressKey, _, err := manager.deriveKeyFromPath( addressKey, _, _, err := manager.deriveKeyFromPath(
ns, info.managedAddr.InternalAccount(), ns, info.managedAddr.InternalAccount(),
info.branch, info.index, true, info.branch, info.index, true,
) )
@ -1276,6 +1299,25 @@ func ValidateAccountName(name string) error {
return nil return nil
} }
// LookupAccount returns the corresponding key scope and account number for the
// account with the given name.
func (m *Manager) LookupAccount(ns walletdb.ReadBucket, name string) (KeyScope,
uint32, error) {
m.mtx.RLock()
defer m.mtx.RUnlock()
for keyScope, scopedMgr := range m.scopedManagers {
acct, err := scopedMgr.LookupAccount(ns, name)
if err == nil {
return keyScope, acct, nil
}
}
str := fmt.Sprintf("account name '%s' not found", name)
return KeyScope{}, 0, managerError(ErrAccountNotFound, str, nil)
}
// selectCryptoKey selects the appropriate crypto key based on the key type. An // selectCryptoKey selects the appropriate crypto key based on the key type. An
// error is returned when an invalid key type is specified or the requested key // error is returned when an invalid key type is specified or the requested key
// requires the manager to be unlocked when it isn't. // requires the manager to be unlocked when it isn't.

View file

@ -72,6 +72,12 @@ type DerivationPath struct {
// Index is the final child in the derivation path. This denotes the // Index is the final child in the derivation path. This denotes the
// key index within as a child of the account and branch. // key index within as a child of the account and branch.
Index uint32 Index uint32
// MasterKeyFingerprint represents the fingerprint of the root key (also
// known as the key with derivation path m/) corresponding to the
// account public key. This may be required by some hardware wallets for
// proper identification and signing.
MasterKeyFingerprint uint32
} }
// KeyScope represents a restricted key scope from the primary root key within // KeyScope represents a restricted key scope from the primary root key within
@ -444,10 +450,11 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
index-- index--
} }
lastExtAddrPath := DerivationPath{ lastExtAddrPath := DerivationPath{
InternalAccount: account, InternalAccount: account,
Account: acctInfo.acctKeyPub.ChildIndex(), Account: acctInfo.acctKeyPub.ChildIndex(),
Branch: branch, Branch: branch,
Index: index, Index: index,
MasterKeyFingerprint: acctInfo.masterKeyFingerprint,
} }
lastExtKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) lastExtKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey)
if err != nil { if err != nil {
@ -465,10 +472,11 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
index-- index--
} }
lastIntAddrPath := DerivationPath{ lastIntAddrPath := DerivationPath{
InternalAccount: account, InternalAccount: account,
Account: acctInfo.acctKeyPub.ChildIndex(), Account: acctInfo.acctKeyPub.ChildIndex(),
Branch: branch, Branch: branch,
Index: index, Index: index,
MasterKeyFingerprint: acctInfo.masterKeyFingerprint,
} }
lastIntKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey) lastIntKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey)
if err != nil { if err != nil {
@ -580,7 +588,7 @@ func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket,
watchOnly := s.rootManager.WatchOnly() watchOnly := s.rootManager.WatchOnly()
private := !s.rootManager.IsLocked() && !watchOnly private := !s.rootManager.IsLocked() && !watchOnly
addrKey, _, err := s.deriveKeyFromPath( addrKey, _, _, err := s.deriveKeyFromPath(
ns, kp.InternalAccount, kp.Branch, kp.Index, private, ns, kp.InternalAccount, kp.Branch, kp.Index, private,
) )
if err != nil { if err != nil {
@ -601,18 +609,18 @@ func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket,
// This function MUST be called with the manager lock held for writes. // This function MUST be called with the manager lock held for writes.
func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket, func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket,
internalAccount, branch, index uint32, private bool) ( internalAccount, branch, index uint32, private bool) (
*hdkeychain.ExtendedKey, *hdkeychain.ExtendedKey, error) { *hdkeychain.ExtendedKey, *hdkeychain.ExtendedKey, uint32, error) {
// Look up the account key information. // Look up the account key information.
acctInfo, err := s.loadAccountInfo(ns, internalAccount) acctInfo, err := s.loadAccountInfo(ns, internalAccount)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, 0, err
} }
private = private && acctInfo.acctKeyPriv != nil private = private && acctInfo.acctKeyPriv != nil
addrKey, err := s.deriveKey(acctInfo, branch, index, private) addrKey, err := s.deriveKey(acctInfo, branch, index, private)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, 0, err
} }
acctKey := acctInfo.acctKeyPub acctKey := acctInfo.acctKeyPub
@ -620,7 +628,7 @@ func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket,
acctKey = acctInfo.acctKeyPriv acctKey = acctInfo.acctKeyPriv
} }
return addrKey, acctKey, nil return addrKey, acctKey, acctInfo.masterKeyFingerprint, nil
} }
// chainAddressRowToManaged returns a new managed address based on chained // chainAddressRowToManaged returns a new managed address based on chained
@ -634,7 +642,7 @@ func (s *ScopedKeyManager) chainAddressRowToManaged(ns walletdb.ReadBucket,
// function, we use the internal isLocked to avoid a deadlock. // function, we use the internal isLocked to avoid a deadlock.
private := !s.rootManager.isLocked() && !s.rootManager.watchOnly() private := !s.rootManager.isLocked() && !s.rootManager.watchOnly()
addressKey, acctKey, err := s.deriveKeyFromPath( addressKey, acctKey, masterKeyFingerprint, err := s.deriveKeyFromPath(
ns, row.account, row.branch, row.index, private, ns, row.account, row.branch, row.index, private,
) )
if err != nil { if err != nil {
@ -647,10 +655,11 @@ func (s *ScopedKeyManager) chainAddressRowToManaged(ns walletdb.ReadBucket,
} }
return s.keyToManaged( return s.keyToManaged(
addressKey, DerivationPath{ addressKey, DerivationPath{
InternalAccount: row.account, InternalAccount: row.account,
Account: acctKey.ChildIndex(), Account: acctKey.ChildIndex(),
Branch: row.branch, Branch: row.branch,
Index: row.index, Index: row.index,
MasterKeyFingerprint: masterKeyFingerprint,
}, acctInfo, }, acctInfo,
) )
} }
@ -2177,6 +2186,22 @@ func (s *ScopedKeyManager) ForEachInternalActiveAddress(ns walletdb.ReadBucket,
return nil return nil
} }
// IsWatchOnlyAccount determines if the given account belonging to this scoped
// manager is set up as watch-only.
func (s *ScopedKeyManager) IsWatchOnlyAccount(ns walletdb.ReadBucket,
account uint32) (bool, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
acctInfo, err := s.loadAccountInfo(ns, account)
if err != nil {
return false, err
}
return acctInfo.acctKeyPriv == nil, nil
}
// cloneKeyWithVersion clones an extended key to use the version corresponding // cloneKeyWithVersion clones an extended key to use the version corresponding
// to the manager's key scope. This should only be used for non-watch-only // to the manager's key scope. This should only be used for non-watch-only
// accounts as they are stored within the database using the legacy BIP-0044 // accounts as they are stored within the database using the legacy BIP-0044

View file

@ -15,6 +15,7 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wallet/txsizes"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/btcwallet/wtxmgr"
) )
@ -99,14 +100,17 @@ func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) {
// txToOutputs creates a signed transaction which includes each output from // txToOutputs creates a signed transaction which includes each output from
// outputs. Previous outputs to reedeem are chosen from the passed account's // outputs. Previous outputs to reedeem are chosen from the passed account's
// UTXO set and minconf policy. An additional output may be added to return // UTXO set and minconf policy. An additional output may be added to return
// change to the wallet. An appropriate fee is included based on the wallet's // change to the wallet. This output will have an address generated from the
// current relay fee. The wallet must be unlocked to create the transaction. // given key scope and account. If a key scope is not specified, the address
// will always be generated from the P2WKH key scope. An appropriate fee is
// included based on the wallet's current relay fee. The wallet must be
// unlocked to create the transaction.
// //
// NOTE: The dryRun argument can be set true to create a tx that doesn't alter // NOTE: The dryRun argument can be set true to create a tx that doesn't alter
// the database. A tx created with this set to true will intentionally have no // the database. A tx created with this set to true will intentionally have no
// input scripts added and SHOULD NOT be broadcasted. // input scripts added and SHOULD NOT be broadcasted.
func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, func (w *Wallet) txToOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) ( account uint32, minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) (
tx *txauthor.AuthoredTx, err error) { tx *txauthor.AuthoredTx, err error) {
chainClient, err := w.requireChainClient() chainClient, err := w.requireChainClient()
@ -120,7 +124,12 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
} }
defer func() { _ = dbtx.Rollback() }() defer func() { _ = dbtx.Rollback() }()
addrmgrNs, changeSource := w.addrMgrWithChangeSource(dbtx, account) addrmgrNs, changeSource, err := w.addrMgrWithChangeSource(
dbtx, keyScope, account,
)
if err != nil {
return nil, err
}
// Get current block's height and hash. // Get current block's height and hash.
bs, err := chainClient.BlockStamp() bs, err := chainClient.BlockStamp()
@ -128,14 +137,17 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
return nil, err return nil, err
} }
eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) eligible, err := w.findEligibleOutputs(
dbtx, keyScope, account, minconf, bs,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
inputSource := makeInputSource(eligible) inputSource := makeInputSource(eligible)
tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb, tx, err = txauthor.NewUnsignedTransaction(
inputSource, changeSource) outputs, feeSatPerKb, inputSource, changeSource,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -155,14 +167,36 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
return tx, nil return tx, nil
} }
err = tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) // Before committing the transaction, we'll sign our inputs. If the
// inputs are part of a watch-only account, there's no private key
// information stored, so we'll skip signing such.
var watchOnly bool
if keyScope == nil {
// If a key scope wasn't specified, then coin selection was
// performed from the default wallet accounts (NP2WKH, P2WKH),
// so any key scope provided doesn't impact the result of this
// call.
watchOnly, err = w.Manager.IsWatchOnlyAccount(
addrmgrNs, waddrmgr.KeyScopeBIP0084, account,
)
} else {
watchOnly, err = w.Manager.IsWatchOnlyAccount(
addrmgrNs, *keyScope, account,
)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !watchOnly {
err = tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs})
if err != nil {
return nil, err
}
err = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues) err = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues)
if err != nil { if err != nil {
return nil, err return nil, err
}
} }
if err := dbtx.Commit(); err != nil { if err := dbtx.Commit(); err != nil {
@ -193,7 +227,10 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
return tx, nil return tx, nil
} }
func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) { func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx,
keyScope *waddrmgr.KeyScope, account uint32, minconf int32,
bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) {
addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey)
txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
@ -239,8 +276,14 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minco
if err != nil || len(addrs) != 1 { if err != nil || len(addrs) != 1 {
continue continue
} }
_, addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0]) scopedMgr, addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0])
if err != nil || addrAcct != account { if err != nil {
continue
}
if keyScope != nil && scopedMgr.Scope() != *keyScope {
continue
}
if addrAcct != account {
continue continue
} }
eligible = append(eligible, *output) eligible = append(eligible, *output)
@ -249,26 +292,61 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minco
} }
// addrMgrWithChangeSource returns the address manager bucket and a change // addrMgrWithChangeSource returns the address manager bucket and a change
// source function that returns change addresses from said address manager. // source that returns change addresses from said address manager. The change
// addresses will come from the specified key scope and account, unless a key
// scope is not specified. In that case, change addresses will always come from
// the P2WKH key scope.
func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx, func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
account uint32) (walletdb.ReadWriteBucket, txauthor.ChangeSource) { changeKeyScope *waddrmgr.KeyScope, account uint32) (
walletdb.ReadWriteBucket, *txauthor.ChangeSource, error) {
// Determine the address type for change addresses of the given account.
if changeKeyScope == nil {
changeKeyScope = &waddrmgr.KeyScopeBIP0084
}
addrType := waddrmgr.ScopeAddrMap[*changeKeyScope].InternalAddrType
// It's possible for the account to have an address schema override, so
// prefer that if it exists.
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
changeSource := func() ([]byte, error) { scopeMgr, err := w.Manager.FetchScopedKeyManager(*changeKeyScope)
// Derive the change output script. We'll use the default key if err != nil {
// scope responsible for P2WPKH addresses to do so. As a hack to return nil, nil, err
// allow spending from the imported account, change addresses }
// are created from account 0. accountInfo, err := scopeMgr.AccountProperties(addrmgrNs, account)
var changeAddr btcutil.Address if err != nil {
var err error return nil, nil, err
changeKeyScope := waddrmgr.KeyScopeBIP0084 }
if accountInfo.AddrSchema != nil {
addrType = accountInfo.AddrSchema.InternalAddrType
}
// Compute the expected size of the script for the change address type.
var scriptSize int
switch addrType {
case waddrmgr.PubKeyHash:
scriptSize = txsizes.P2PKHPkScriptSize
case waddrmgr.NestedWitnessPubKey:
scriptSize = txsizes.NestedP2WPKHPkScriptSize
case waddrmgr.WitnessPubKey:
scriptSize = txsizes.P2WPKHPkScriptSize
}
newChangeScript := func() ([]byte, error) {
// Derive the change output script. As a hack to allow spending
// from the imported account, change addresses are created from
// account 0.
var (
changeAddr btcutil.Address
err error
)
if account == waddrmgr.ImportedAddrAccount { if account == waddrmgr.ImportedAddrAccount {
changeAddr, err = w.newChangeAddress( changeAddr, err = w.newChangeAddress(
addrmgrNs, 0, changeKeyScope, addrmgrNs, 0, *changeKeyScope,
) )
} else { } else {
changeAddr, err = w.newChangeAddress( changeAddr, err = w.newChangeAddress(
addrmgrNs, account, changeKeyScope, addrmgrNs, account, *changeKeyScope,
) )
} }
if err != nil { if err != nil {
@ -276,7 +354,11 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
} }
return txscript.PayToAddrScript(changeAddr) return txscript.PayToAddrScript(changeAddr)
} }
return addrmgrNs, changeSource
return addrmgrNs, &txauthor.ChangeSource{
ScriptSize: scriptSize,
NewScript: newChangeScript,
}, nil
} }
// validateMsgTx verifies transaction input scripts for tx. All previous output // validateMsgTx verifies transaction input scripts for tx. All previous output

View file

@ -34,7 +34,8 @@ func TestTxToOutputsDryRun(t *testing.T) {
defer cleanup() defer cleanup()
// Create an address we can use to send some coins to. // Create an address we can use to send some coins to.
addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0044) keyScope := waddrmgr.KeyScopeBIP0049Plus
addr, err := w.CurrentAddress(0, keyScope)
if err != nil { if err != nil {
t.Fatalf("unable to get current address: %v", addr) t.Fatalf("unable to get current address: %v", addr)
} }
@ -70,7 +71,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// First do a few dry-runs, making sure the number of addresses in the // First do a few dry-runs, making sure the number of addresses in the
// database us not inflated. // database us not inflated.
dryRunTx, err := w.txToOutputs(txOuts, 0, 1, 1000, true) dryRunTx, err := w.txToOutputs(txOuts, nil, 0, 1, 1000, true)
if err != nil { if err != nil {
t.Fatalf("unable to author tx: %v", err) t.Fatalf("unable to author tx: %v", err)
} }
@ -85,7 +86,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
t.Fatalf("expected 1 address, found %v", len(addresses)) t.Fatalf("expected 1 address, found %v", len(addresses))
} }
dryRunTx2, err := w.txToOutputs(txOuts, 0, 1, 1000, true) dryRunTx2, err := w.txToOutputs(txOuts, nil, 0, 1, 1000, true)
if err != nil { if err != nil {
t.Fatalf("unable to author tx: %v", err) t.Fatalf("unable to author tx: %v", err)
} }
@ -118,7 +119,7 @@ func TestTxToOutputsDryRun(t *testing.T) {
// Now we do a proper, non-dry run. This should add a change address // Now we do a proper, non-dry run. This should add a change address
// to the database. // to the database.
tx, err := w.txToOutputs(txOuts, 0, 1, 1000, false) tx, err := w.txToOutputs(txOuts, nil, 0, 1, 1000, false)
if err != nil { if err != nil {
t.Fatalf("unable to author tx: %v", err) t.Fatalf("unable to author tx: %v", err)
} }

View file

@ -12,8 +12,10 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/psbt" "github.com/btcsuite/btcutil/psbt"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wallet/txrules" "github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/btcwallet/wtxmgr"
) )
@ -24,17 +26,22 @@ import (
// is created and the index -1 is returned. // is created and the index -1 is returned.
// //
// NOTE: If the packet doesn't contain any inputs, coin selection is performed // NOTE: If the packet doesn't contain any inputs, coin selection is performed
// automatically. If the packet does contain any inputs, it is assumed that full // automatically, only selecting inputs from the account based on the given key
// coin selection happened externally and no additional inputs are added. If the // scope and account number. If a key scope is not specified, then inputs from
// specified inputs aren't enough to fund the outputs with the given fee rate, // accounts matching the account number provided across all key scopes may be
// an error is returned. // selected. This is done to handle the default account case, where a user wants
// to fund a PSBT with inputs regardless of their type (NP2WKH, P2WKH, etc.). If
// the packet does contain any inputs, it is assumed that full coin selection
// happened externally and no additional inputs are added. If the specified
// inputs aren't enough to fund the outputs with the given fee rate, an error is
// returned.
// //
// NOTE: A caller of the method should hold the global coin selection lock of // NOTE: A caller of the method should hold the global coin selection lock of
// the wallet. However, no UTXO specific lock lease is acquired for any of the // the wallet. However, no UTXO specific lock lease is acquired for any of the
// selected/validated inputs by this method. It is in the caller's // selected/validated inputs by this method. It is in the caller's
// responsibility to lock the inputs before handing the partial transaction out. // responsibility to lock the inputs before handing the partial transaction out.
func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32, func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
feeSatPerKB btcutil.Amount) (int32, error) { account uint32, feeSatPerKB btcutil.Amount) (int32, error) {
// Make sure the packet is well formed. We only require there to be at // Make sure the packet is well formed. We only require there to be at
// least one output but not necessarily any inputs. // least one output but not necessarily any inputs.
@ -70,7 +77,7 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
addInputInfo := func(inputs []*wire.TxIn) error { addInputInfo := func(inputs []*wire.TxIn) error {
packet.Inputs = make([]psbt.PInput, len(inputs)) packet.Inputs = make([]psbt.PInput, len(inputs))
for idx, in := range inputs { for idx, in := range inputs {
tx, utxo, _, err := w.FetchInputInfo( tx, utxo, derivationPath, _, err := w.FetchInputInfo(
&in.PreviousOutPoint, &in.PreviousOutPoint,
) )
if err != nil { if err != nil {
@ -91,10 +98,27 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
} }
packet.Inputs[idx].SighashType = txscript.SigHashAll packet.Inputs[idx].SighashType = txscript.SigHashAll
// Include the derivation path for each input.
packet.Inputs[idx].Bip32Derivation = []*psbt.Bip32Derivation{
derivationPath,
}
// We don't want to include the witness or any script // We don't want to include the witness or any script
// just yet. // on the unsigned TX just yet.
packet.UnsignedTx.TxIn[idx].Witness = wire.TxWitness{} packet.UnsignedTx.TxIn[idx].Witness = wire.TxWitness{}
packet.UnsignedTx.TxIn[idx].SignatureScript = nil packet.UnsignedTx.TxIn[idx].SignatureScript = nil
// For nested P2WKH we need to add the redeem script to
// the input, otherwise an offline wallet won't be able
// to sign for it. For normal P2WKH this will be nil.
addr, witnessProgram, _, err := w.scriptForOutput(utxo)
if err != nil {
return fmt.Errorf("error fetching UTXO "+
"script: %v", err)
}
if addr.AddrType() == waddrmgr.NestedWitnessPubKey {
packet.Inputs[idx].RedeemScript = witnessProgram
}
} }
return nil return nil
@ -108,8 +132,8 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
// includes everything we need, specifically fee estimation and // includes everything we need, specifically fee estimation and
// change address creation. // change address creation.
tx, err = w.CreateSimpleTx( tx, err = w.CreateSimpleTx(
account, packet.UnsignedTx.TxOut, 1, feeSatPerKB, keyScope, account, packet.UnsignedTx.TxOut, 1,
false, feeSatPerKB, false,
) )
if err != nil { if err != nil {
return 0, fmt.Errorf("error creating funding TX: %v", return 0, fmt.Errorf("error creating funding TX: %v",
@ -161,7 +185,12 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
if err != nil { if err != nil {
return 0, err return 0, err
} }
_, changeSource := w.addrMgrWithChangeSource(dbtx, account) _, changeSource, err := w.addrMgrWithChangeSource(
dbtx, keyScope, account,
)
if err != nil {
return 0, err
}
// Ask the txauthor to create a transaction with our selected // Ask the txauthor to create a transaction with our selected
// coins. This will perform fee estimation and add a change // coins. This will perform fee estimation and add a change
@ -227,7 +256,9 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
// //
// NOTE: This method does NOT publish the transaction after it's been finalized // NOTE: This method does NOT publish the transaction after it's been finalized
// successfully. // successfully.
func (w *Wallet) FinalizePsbt(packet *psbt.Packet) error { func (w *Wallet) FinalizePsbt(keyScope *waddrmgr.KeyScope, account uint32,
packet *psbt.Packet) error {
// Let's check that this is actually something we can and want to sign. // Let's check that this is actually something we can and want to sign.
// We need at least one input and one output. // We need at least one input and one output.
err := psbt.VerifyInputOutputLen(packet, true, true) err := psbt.VerifyInputOutputLen(packet, true, true)
@ -259,7 +290,7 @@ func (w *Wallet) FinalizePsbt(packet *psbt.Packet) error {
// We can only sign this input if it's ours, so we try to map it // We can only sign this input if it's ours, so we try to map it
// to a coin we own. If we can't, then we'll continue as it // to a coin we own. If we can't, then we'll continue as it
// isn't our input. // isn't our input.
fullTx, txOut, _, err := w.FetchInputInfo( fullTx, txOut, _, _, err := w.FetchInputInfo(
&txIn.PreviousOutPoint, &txIn.PreviousOutPoint,
) )
if err != nil { if err != nil {
@ -298,8 +329,37 @@ func (w *Wallet) FinalizePsbt(packet *psbt.Packet) error {
} }
} }
// Finally, we'll sign the input as is, and populate the input // Finally, if the input doesn't belong to a watch-only account,
// with the witness and sigScript (if needed). // then we'll sign it as is, and populate the input with the
// witness and sigScript (if needed).
watchOnly := false
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
var err error
if keyScope == nil {
// If a key scope wasn't specified, then coin
// selection was performed from the default
// wallet accounts (NP2WKH, P2WKH), so any key
// scope provided doesn't impact the result of
// this call.
watchOnly, err = w.Manager.IsWatchOnlyAccount(
ns, waddrmgr.KeyScopeBIP0084, account,
)
} else {
watchOnly, err = w.Manager.IsWatchOnlyAccount(
ns, *keyScope, account,
)
}
return err
})
if err != nil {
return fmt.Errorf("unable to determine if account is "+
"watch-only: %v", err)
}
if watchOnly {
continue
}
witness, sigScript, err := w.ComputeInputScript( witness, sigScript, err := w.ComputeInputScript(
tx, signOutput, idx, sigHashes, in.SighashType, nil, tx, signOutput, idx, sigHashes, in.SighashType, nil,
) )

View file

@ -219,7 +219,7 @@ func TestFundPsbt(t *testing.T) {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
changeIndex, err := w.FundPsbt( changeIndex, err := w.FundPsbt(
tc.packet, 0, tc.feeRateSatPerKB, tc.packet, nil, 0, tc.feeRateSatPerKB,
) )
// In any case, unlock the UTXO before continuing, we // In any case, unlock the UTXO before continuing, we
@ -391,7 +391,7 @@ func TestFinalizePsbt(t *testing.T) {
} }
// Finalize it to add all witness data then extract the final TX. // Finalize it to add all witness data then extract the final TX.
err = w.FinalizePsbt(packet) err = w.FinalizePsbt(nil, 0, packet)
if err != nil { if err != nil {
t.Fatalf("error finalizing PSBT packet: %v", err) t.Fatalf("error finalizing PSBT packet: %v", err)
} }

View file

@ -5,6 +5,8 @@
package wallet package wallet
import ( import (
"fmt"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@ -12,6 +14,71 @@ import (
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
) )
// scriptForOutput returns the address, witness program and redeem script for a
// given UTXO. An error is returned if the UTXO does not belong to our wallet or
// it is not a managed pubKey address.
func (w *Wallet) scriptForOutput(output *wire.TxOut) (
waddrmgr.ManagedPubKeyAddress, []byte, []byte, error) {
// First make sure we can sign for the input by making sure the script
// in the UTXO belongs to our wallet and we have the private key for it.
walletAddr, err := w.fetchOutputAddr(output.PkScript)
if err != nil {
return nil, nil, nil, err
}
pubKeyAddr, ok := walletAddr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return nil, nil, nil, fmt.Errorf("address %s is not a "+
"p2wkh or np2wkh address", walletAddr.Address())
}
var (
witnessProgram []byte
sigScript []byte
)
switch {
// If we're spending p2wkh output nested within a p2sh output, then
// we'll need to attach a sigScript in addition to witness data.
case walletAddr.AddrType() == waddrmgr.NestedWitnessPubKey:
pubKey := pubKeyAddr.PubKey()
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
// Next, we'll generate a valid sigScript that will allow us to
// spend the p2sh output. The sigScript will contain only a
// single push of the p2wkh witness program corresponding to
// the matching public key of this address.
p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(
pubKeyHash, w.chainParams,
)
if err != nil {
return nil, nil, nil, err
}
witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr)
if err != nil {
return nil, nil, nil, err
}
bldr := txscript.NewScriptBuilder()
bldr.AddData(witnessProgram)
sigScript, err = bldr.Script()
if err != nil {
return nil, nil, nil, err
}
// Otherwise, this is a regular p2wkh output, so we include the
// witness program itself as the subscript to generate the proper
// sighash digest. As part of the new sighash digest algorithm, the
// p2wkh witness program will be expanded into a regular p2kh
// script.
default:
witnessProgram = output.PkScript
}
return pubKeyAddr, witnessProgram, sigScript, nil
}
// PrivKeyTweaker is a function type that can be used to pass in a callback for // PrivKeyTweaker is a function type that can be used to pass in a callback for
// tweaking a private key before it's used to sign an input. // tweaking a private key before it's used to sign an input.
type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error) type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error)
@ -25,62 +92,16 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness, hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness,
[]byte, error) { []byte, error) {
// First make sure we can sign for the input by making sure the script walletAddr, witnessProgram, sigScript, err := w.scriptForOutput(output)
// in the UTXO belongs to our wallet and we have the private key for it.
walletAddr, err := w.fetchOutputAddr(output.PkScript)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
pka := walletAddr.(waddrmgr.ManagedPubKeyAddress) privKey, err := walletAddr.PrivKey()
privKey, err := pka.PrivKey()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
var (
witnessProgram []byte
sigScript []byte
)
switch {
// If we're spending p2wkh output nested within a p2sh output, then
// we'll need to attach a sigScript in addition to witness data.
case pka.AddrType() == waddrmgr.NestedWitnessPubKey:
pubKey := privKey.PubKey()
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
// Next, we'll generate a valid sigScript that will allow us to
// spend the p2sh output. The sigScript will contain only a
// single push of the p2wkh witness program corresponding to
// the matching public key of this address.
p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(
pubKeyHash, w.chainParams,
)
if err != nil {
return nil, nil, err
}
witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr)
if err != nil {
return nil, nil, err
}
bldr := txscript.NewScriptBuilder()
bldr.AddData(witnessProgram)
sigScript, err = bldr.Script()
if err != nil {
return nil, nil, err
}
// Otherwise, this is a regular p2wkh output, so we include the
// witness program itself as the subscript to generate the proper
// sighash digest. As part of the new sighash digest algorithm, the
// p2wkh witness program will be expanded into a regular p2kh
// script.
default:
witnessProgram = output.PkScript
}
// If we need to maybe tweak our private key, do it now. // If we need to maybe tweak our private key, do it now.
if tweaker != nil { if tweaker != nil {
privKey, err = tweaker(privKey) privKey, err = tweaker(privKey)

View file

@ -60,8 +60,15 @@ type AuthoredTx struct {
ChangeIndex int // negative if no change ChangeIndex int // negative if no change
} }
// ChangeSource provides P2PKH change output scripts for transaction creation. // ChangeSource provides change output scripts for transaction creation.
type ChangeSource func() ([]byte, error) type ChangeSource struct {
// NewScript is a closure that produces unique change output scripts per
// invocation.
NewScript func() ([]byte, error)
// ScriptSize is the size in bytes of scripts produced by `NewScript`.
ScriptSize int
}
// NewUnsignedTransaction creates an unsigned transaction paying to one or more // NewUnsignedTransaction creates an unsigned transaction paying to one or more
// non-change outputs. An appropriate transaction fee is included based on the // non-change outputs. An appropriate transaction fee is included based on the
@ -84,10 +91,12 @@ type ChangeSource func() ([]byte, error)
// //
// BUGS: Fee estimation may be off when redeeming non-compressed P2PKH outputs. // BUGS: Fee estimation may be off when redeeming non-compressed P2PKH outputs.
func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount, func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount,
fetchInputs InputSource, fetchChange ChangeSource) (*AuthoredTx, error) { fetchInputs InputSource, changeSource *ChangeSource) (*AuthoredTx, error) {
targetAmount := SumOutputValues(outputs) targetAmount := SumOutputValues(outputs)
estimatedSize := txsizes.EstimateVirtualSize(0, 1, 0, outputs, true) estimatedSize := txsizes.EstimateVirtualSize(
0, 1, 0, outputs, changeSource.ScriptSize,
)
targetFee := txrules.FeeForSerializeSize(feeRatePerKb, estimatedSize) targetFee := txrules.FeeForSerializeSize(feeRatePerKb, estimatedSize)
for { for {
@ -115,8 +124,9 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount,
} }
} }
maxSignedSize := txsizes.EstimateVirtualSize(p2pkh, p2wpkh, maxSignedSize := txsizes.EstimateVirtualSize(
nested, outputs, true) p2pkh, p2wpkh, nested, outputs, changeSource.ScriptSize,
)
maxRequiredFee := txrules.FeeForSerializeSize(feeRatePerKb, maxSignedSize) maxRequiredFee := txrules.FeeForSerializeSize(feeRatePerKb, maxSignedSize)
remainingAmount := inputAmount - targetAmount remainingAmount := inputAmount - targetAmount
if remainingAmount < maxRequiredFee { if remainingAmount < maxRequiredFee {
@ -130,18 +140,16 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount,
TxOut: outputs, TxOut: outputs,
LockTime: 0, LockTime: 0,
} }
changeIndex := -1 changeIndex := -1
changeAmount := inputAmount - targetAmount - maxRequiredFee changeAmount := inputAmount - targetAmount - maxRequiredFee
if changeAmount != 0 && !txrules.IsDustAmount(changeAmount, if changeAmount != 0 && !txrules.IsDustAmount(changeAmount,
txsizes.P2WPKHPkScriptSize, txrules.DefaultRelayFeePerKb) { changeSource.ScriptSize, txrules.DefaultRelayFeePerKb) {
changeScript, err := fetchChange()
changeScript, err := changeSource.NewScript()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(changeScript) > txsizes.P2WPKHPkScriptSize {
return nil, errors.New("fee estimation requires change " +
"scripts no larger than P2WPKH output scripts")
}
change := wire.NewTxOut(int64(changeAmount), changeScript) change := wire.NewTxOut(int64(changeAmount), changeScript)
l := len(outputs) l := len(outputs)
unsignedTransaction.TxOut = append(outputs[:l:l], change) unsignedTransaction.TxOut = append(outputs[:l:l], change)

View file

@ -61,7 +61,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
Outputs: p2pkhOutputs(1e6), Outputs: p2pkhOutputs(1e6),
RelayFee: 1e3, RelayFee: 1e3,
ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e3, ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6), true)), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6), txsizes.P2WPKHPkScriptSize)),
InputCount: 1, InputCount: 1,
}, },
2: { 2: {
@ -69,7 +69,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
Outputs: p2pkhOutputs(1e6), Outputs: p2pkhOutputs(1e6),
RelayFee: 1e4, RelayFee: 1e4,
ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e4, ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e4,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6), true)), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6), txsizes.P2WPKHPkScriptSize)),
InputCount: 1, InputCount: 1,
}, },
3: { 3: {
@ -77,7 +77,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
Outputs: p2pkhOutputs(1e6, 1e6, 1e6), Outputs: p2pkhOutputs(1e6, 1e6, 1e6),
RelayFee: 1e4, RelayFee: 1e4,
ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(1e4, ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(1e4,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6, 1e6, 1e6), true)), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6, 1e6, 1e6), txsizes.P2WPKHPkScriptSize)),
InputCount: 1, InputCount: 1,
}, },
4: { 4: {
@ -85,7 +85,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
Outputs: p2pkhOutputs(1e6, 1e6, 1e6), Outputs: p2pkhOutputs(1e6, 1e6, 1e6),
RelayFee: 2.55e3, RelayFee: 2.55e3,
ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(2.55e3, ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(2.55e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6, 1e6, 1e6), true)), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6, 1e6, 1e6), txsizes.P2WPKHPkScriptSize)),
InputCount: 1, InputCount: 1,
}, },
@ -93,7 +93,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
5: { 5: {
UnspentOutputs: p2pkhOutputs(1e8), UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3, Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true))), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), txsizes.P2WPKHPkScriptSize))),
RelayFee: 1e3, RelayFee: 1e3,
ChangeAmount: 545, ChangeAmount: 545,
InputCount: 1, InputCount: 1,
@ -101,7 +101,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
6: { 6: {
UnspentOutputs: p2pkhOutputs(1e8), UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3, Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true))), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), txsizes.P2WPKHPkScriptSize))),
RelayFee: 1e3, RelayFee: 1e3,
ChangeAmount: 546, ChangeAmount: 546,
InputCount: 1, InputCount: 1,
@ -111,7 +111,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
7: { 7: {
UnspentOutputs: p2pkhOutputs(1e8), UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 1392 - txrules.FeeForSerializeSize(2.55e3, Outputs: p2pkhOutputs(1e8 - 1392 - txrules.FeeForSerializeSize(2.55e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true))), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), txsizes.P2WPKHPkScriptSize))),
RelayFee: 2.55e3, RelayFee: 2.55e3,
ChangeAmount: 1392, ChangeAmount: 1392,
InputCount: 1, InputCount: 1,
@ -119,7 +119,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
8: { 8: {
UnspentOutputs: p2pkhOutputs(1e8), UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 1393 - txrules.FeeForSerializeSize(2.55e3, Outputs: p2pkhOutputs(1e8 - 1393 - txrules.FeeForSerializeSize(2.55e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true))), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), txsizes.P2WPKHPkScriptSize))),
RelayFee: 2.55e3, RelayFee: 2.55e3,
ChangeAmount: 1393, ChangeAmount: 1393,
InputCount: 1, InputCount: 1,
@ -131,7 +131,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
9: { 9: {
UnspentOutputs: p2pkhOutputs(1e8, 1e8), UnspentOutputs: p2pkhOutputs(1e8, 1e8),
Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3, Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true))), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), txsizes.P2WPKHPkScriptSize))),
RelayFee: 1e3, RelayFee: 1e3,
ChangeAmount: 546, ChangeAmount: 546,
InputCount: 1, InputCount: 1,
@ -145,7 +145,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
10: { 10: {
UnspentOutputs: p2pkhOutputs(1e8, 1e8), UnspentOutputs: p2pkhOutputs(1e8, 1e8),
Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3, Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true))), txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), txsizes.P2WPKHPkScriptSize))),
RelayFee: 1e3, RelayFee: 1e3,
ChangeAmount: 545, ChangeAmount: 545,
InputCount: 1, InputCount: 1,
@ -157,7 +157,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
Outputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e8),
RelayFee: 1e3, RelayFee: 1e3,
ChangeAmount: 1e8 - txrules.FeeForSerializeSize(1e3, ChangeAmount: 1e8 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateVirtualSize(2, 0, 0, p2pkhOutputs(1e8), true)), txsizes.EstimateVirtualSize(2, 0, 0, p2pkhOutputs(1e8), txsizes.P2WPKHPkScriptSize)),
InputCount: 2, InputCount: 2,
}, },
@ -172,9 +172,12 @@ func TestNewUnsignedTransaction(t *testing.T) {
}, },
} }
changeSource := func() ([]byte, error) { changeSource := &ChangeSource{
// Only length matters for these tests. NewScript: func() ([]byte, error) {
return make([]byte, txsizes.P2WPKHPkScriptSize), nil // Only length matters for these tests.
return make([]byte, txsizes.P2WPKHPkScriptSize), nil
},
ScriptSize: txsizes.P2WPKHPkScriptSize,
} }
for i, test := range tests { for i, test := range tests {

View file

@ -82,6 +82,16 @@ const (
// - 4 bytes sequence // - 4 bytes sequence
RedeemP2WPKHInputSize = 32 + 4 + 1 + RedeemP2WPKHScriptSize + 4 RedeemP2WPKHInputSize = 32 + 4 + 1 + RedeemP2WPKHScriptSize + 4
// NestedP2WPKHPkScriptSize is the size of a transaction output script
// that pays to a pay-to-witness-key hash nested in P2SH (P2SH-P2WPKH).
// It is calculated as:
//
// - OP_HASH160
// - OP_DATA_20
// - 20 bytes script hash
// - OP_EQUAL
NestedP2WPKHPkScriptSize = 1 + 1 + 20 + 1
// RedeemNestedP2WPKHScriptSize is the worst case size of a transaction // RedeemNestedP2WPKHScriptSize is the worst case size of a transaction
// input script that redeems a pay-to-witness-key hash nested in P2SH // input script that redeems a pay-to-witness-key hash nested in P2SH
// (P2SH-P2WPKH). It is calculated as: // (P2SH-P2WPKH). It is calculated as:
@ -150,12 +160,14 @@ func EstimateSerializeSize(inputCount int, txOuts []*wire.TxOut, addChangeOutput
// from txOuts. The estimate is incremented for an additional P2PKH // from txOuts. The estimate is incremented for an additional P2PKH
// change output if addChangeOutput is true. // change output if addChangeOutput is true.
func EstimateVirtualSize(numP2PKHIns, numP2WPKHIns, numNestedP2WPKHIns int, func EstimateVirtualSize(numP2PKHIns, numP2WPKHIns, numNestedP2WPKHIns int,
txOuts []*wire.TxOut, addChangeOutput bool) int { txOuts []*wire.TxOut, changeScriptSize int) int {
changeSize := 0
outputCount := len(txOuts) outputCount := len(txOuts)
if addChangeOutput {
// We are always using P2WPKH as change output. changeOutputSize := 0
changeSize = P2WPKHOutputSize if changeScriptSize > 0 {
changeOutputSize = 8 +
wire.VarIntSerializeSize(uint64(changeScriptSize)) +
changeScriptSize
outputCount++ outputCount++
} }
@ -170,7 +182,7 @@ func EstimateVirtualSize(numP2PKHIns, numP2WPKHIns, numNestedP2WPKHIns int,
numP2WPKHIns*RedeemP2WPKHInputSize + numP2WPKHIns*RedeemP2WPKHInputSize +
numNestedP2WPKHIns*RedeemNestedP2WPKHInputSize + numNestedP2WPKHIns*RedeemNestedP2WPKHInputSize +
SumOutputSerializeSizes(txOuts) + SumOutputSerializeSizes(txOuts) +
changeSize changeOutputSize
// If this transaction has any witness inputs, we must count the // If this transaction has any witness inputs, we must count the
// witness data. // witness data.

View file

@ -163,8 +163,12 @@ func TestEstimateVirtualSize(t *testing.T) {
t.Fatalf("unable to get test tx: %v", err) t.Fatalf("unable to get test tx: %v", err)
} }
changeScriptSize := 0
if test.change {
changeScriptSize = P2WPKHPkScriptSize
}
est := EstimateVirtualSize(test.p2pkhIns, test.p2wpkhIns, est := EstimateVirtualSize(test.p2pkhIns, test.p2wpkhIns,
test.nestedp2wpkhIns, tx.TxOut, test.change) test.nestedp2wpkhIns, tx.TxOut, changeScriptSize)
if est != test.result { if est != test.result {
t.Fatalf("expected estimated vsize to be %d, "+ t.Fatalf("expected estimated vsize to be %d, "+

View file

@ -11,6 +11,8 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcutil/psbt"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
) )
@ -105,15 +107,15 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut
// full transaction, the target txout and the number of confirmations are // full transaction, the target txout and the number of confirmations are
// returned. Otherwise, a non-nil error value of ErrNotMine is returned instead. // returned. Otherwise, a non-nil error value of ErrNotMine is returned instead.
func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx,
*wire.TxOut, int64, error) { *wire.TxOut, *psbt.Bip32Derivation, int64, error) {
// We manually look up the output within the tx store. // We manually look up the output within the tx store.
txid := &prevOut.Hash txid := &prevOut.Hash
txDetail, err := UnstableAPI(w).TxDetails(txid) txDetail, err := UnstableAPI(w).TxDetails(txid)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, nil, 0, err
} else if txDetail == nil { } else if txDetail == nil {
return nil, nil, 0, ErrNotMine return nil, nil, nil, 0, ErrNotMine
} }
// With the output retrieved, we'll make an additional check to ensure // With the output retrieved, we'll make an additional check to ensure
@ -122,19 +124,25 @@ func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx,
// like in the event of us being the sender of the transaction. // like in the event of us being the sender of the transaction.
numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut)) numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut))
if prevOut.Index >= numOutputs { if prevOut.Index >= numOutputs {
return nil, nil, 0, fmt.Errorf("invalid output index %v for "+ return nil, nil, nil, 0, fmt.Errorf("invalid output index %v for "+
"transaction with %v outputs", prevOut.Index, "transaction with %v outputs", prevOut.Index,
numOutputs) numOutputs)
} }
pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript
if _, err := w.fetchOutputAddr(pkScript); err != nil { addr, err := w.fetchOutputAddr(pkScript)
return nil, nil, 0, err if err != nil {
return nil, nil, nil, 0, err
} }
pubKeyAddr, ok := addr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return nil, nil, nil, 0, err
}
keyScope, derivationPath, _ := pubKeyAddr.DerivationInfo()
// Determine the number of confirmations the output currently has. // Determine the number of confirmations the output currently has.
_, currentHeight, err := w.chainClient.GetBestBlock() _, currentHeight, err := w.chainClient.GetBestBlock()
if err != nil { if err != nil {
return nil, nil, 0, fmt.Errorf("unable to retrieve current "+ return nil, nil, nil, 0, fmt.Errorf("unable to retrieve current "+
"height: %v", err) "height: %v", err)
} }
confs := int64(0) confs := int64(0)
@ -143,9 +151,19 @@ func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx,
} }
return &txDetail.TxRecord.MsgTx, &wire.TxOut{ return &txDetail.TxRecord.MsgTx, &wire.TxOut{
Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value, Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value,
PkScript: pkScript, PkScript: pkScript,
}, confs, nil }, &psbt.Bip32Derivation{
PubKey: pubKeyAddr.PubKey().SerializeCompressed(),
MasterKeyFingerprint: derivationPath.MasterKeyFingerprint,
Bip32Path: []uint32{
keyScope.Purpose + hdkeychain.HardenedKeyStart,
keyScope.Coin + hdkeychain.HardenedKeyStart,
derivationPath.Account,
derivationPath.Branch,
derivationPath.Index,
},
}, confs, nil
} }
// fetchOutputAddr attempts to fetch the managed address corresponding to the // fetchOutputAddr attempts to fetch the managed address corresponding to the

View file

@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
) )
@ -43,7 +44,7 @@ func TestFetchInputInfo(t *testing.T) {
Hash: incomingTx.TxHash(), Hash: incomingTx.TxHash(),
Index: 0, Index: 0,
} }
tx, out, confirmations, err := w.FetchInputInfo(prevOut) tx, out, derivationPath, confirmations, err := w.FetchInputInfo(prevOut)
if err != nil { if err != nil {
t.Fatalf("error fetching input info: %v", err) t.Fatalf("error fetching input info: %v", err)
} }
@ -54,6 +55,34 @@ func TestFetchInputInfo(t *testing.T) {
t.Fatalf("unexpected TX out, got %v wanted %v", t.Fatalf("unexpected TX out, got %v wanted %v",
tx.TxOut[prevOut.Index].PkScript, utxOut) tx.TxOut[prevOut.Index].PkScript, utxOut)
} }
if len(derivationPath.Bip32Path) != 5 {
t.Fatalf("expected derivation path of length %v, got %v", 3,
len(derivationPath.Bip32Path))
}
if derivationPath.Bip32Path[0] !=
waddrmgr.KeyScopeBIP0084.Purpose+hdkeychain.HardenedKeyStart {
t.Fatalf("expected purpose %v, got %v",
waddrmgr.KeyScopeBIP0084.Purpose,
derivationPath.Bip32Path[0])
}
if derivationPath.Bip32Path[1] !=
waddrmgr.KeyScopeBIP0084.Coin+hdkeychain.HardenedKeyStart {
t.Fatalf("expected coin type %v, got %v",
waddrmgr.KeyScopeBIP0084.Coin,
derivationPath.Bip32Path[1])
}
if derivationPath.Bip32Path[2] != hdkeychain.HardenedKeyStart {
t.Fatalf("expected account %v, got %v",
hdkeychain.HardenedKeyStart, derivationPath.Bip32Path[2])
}
if derivationPath.Bip32Path[3] != 0 {
t.Fatalf("expected branch %v, got %v", 0,
derivationPath.Bip32Path[3])
}
if derivationPath.Bip32Path[4] != 0 {
t.Fatalf("expected index %v, got %v", 0,
derivationPath.Bip32Path[4])
}
if confirmations != int64(0-testBlockHeight) { if confirmations != int64(0-testBlockHeight) {
t.Fatalf("unexpected number of confirmations, got %d wanted %d", t.Fatalf("unexpected number of confirmations, got %d wanted %d",
confirmations, 0-testBlockHeight) confirmations, 0-testBlockHeight)

View file

@ -1125,6 +1125,7 @@ func logFilterBlocksResp(block wtxmgr.BlockMeta,
type ( type (
createTxRequest struct { createTxRequest struct {
keyScope *waddrmgr.KeyScope
account uint32 account uint32
outputs []*wire.TxOut outputs []*wire.TxOut
minconf int32 minconf int32
@ -1159,8 +1160,10 @@ out:
txr.resp <- createTxResponse{nil, err} txr.resp <- createTxResponse{nil, err}
continue continue
} }
tx, err := w.txToOutputs(txr.outputs, txr.account, tx, err := w.txToOutputs(
txr.minconf, txr.feeSatPerKB, txr.dryRun) txr.outputs, txr.keyScope, txr.account,
txr.minconf, txr.feeSatPerKB, txr.dryRun,
)
heldUnlock.release() heldUnlock.release()
txr.resp <- createTxResponse{tx, err} txr.resp <- createTxResponse{tx, err}
case <-quit: case <-quit:
@ -1170,20 +1173,25 @@ out:
w.wg.Done() w.wg.Done()
} }
// CreateSimpleTx creates a new signed transaction spending unspent P2PKH // CreateSimpleTx creates a new signed transaction spending unspent outputs with
// outputs with at least minconf confirmations spending to any number of // at least minconf confirmations spending to any number of address/amount
// address/amount pairs. Change and an appropriate transaction fee are // pairs. Only unspent outputs belonging to the given key scope and account will
// automatically included, if necessary. All transaction creation through this // be selected, unless a key scope is not specified. In that case, inputs from all
// function is serialized to prevent the creation of many transactions which // accounts may be selected, no matter what key scope they belong to. This is
// spend the same outputs. // done to handle the default account case, where a user wants to fund a PSBT
// with inputs regardless of their type (NP2WKH, P2WKH, etc.). 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.
// //
// NOTE: The dryRun argument can be set true to create a tx that doesn't alter // NOTE: The dryRun argument can be set true to create a tx that doesn't alter
// the database. A tx created with this set to true SHOULD NOT be broadcasted. // the database. A tx created with this set to true SHOULD NOT be broadcasted.
func (w *Wallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut, func (w *Wallet) CreateSimpleTx(keyScope *waddrmgr.KeyScope, account uint32,
minconf int32, satPerKb btcutil.Amount, dryRun bool) ( outputs []*wire.TxOut, minconf int32, satPerKb btcutil.Amount,
*txauthor.AuthoredTx, error) { dryRun bool) (*txauthor.AuthoredTx, error) {
req := createTxRequest{ req := createTxRequest{
keyScope: keyScope,
account: account, account: account,
outputs: outputs, outputs: outputs,
minconf: minconf, minconf: minconf,
@ -1754,6 +1762,46 @@ func (w *Wallet) AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddr
return props, err return props, err
} }
// AccountPropertiesByName returns the properties of an account by its name. It
// first fetches the desynced information from the address manager, then updates
// the indexes based on the address pools.
func (w *Wallet) AccountPropertiesByName(scope waddrmgr.KeyScope,
name string) (*waddrmgr.AccountProperties, error) {
manager, err := w.Manager.FetchScopedKeyManager(scope)
if err != nil {
return nil, err
}
var props *waddrmgr.AccountProperties
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
acct, err := manager.LookupAccount(waddrmgrNs, name)
if err != nil {
return err
}
props, err = manager.AccountProperties(waddrmgrNs, acct)
return err
})
return props, err
}
// LookupAccount returns the corresponding key scope and account number for the
// account with the given name.
func (w *Wallet) LookupAccount(name string) (waddrmgr.KeyScope, uint32, error) {
var (
keyScope waddrmgr.KeyScope
account uint32
)
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
var err error
keyScope, account, err = w.Manager.LookupAccount(ns, name)
return err
})
return keyScope, account, err
}
// RenameAccount sets the name for an account number to newName. // RenameAccount sets the name for an account number to newName.
func (w *Wallet) RenameAccount(scope waddrmgr.KeyScope, account uint32, newName string) error { func (w *Wallet) RenameAccount(scope waddrmgr.KeyScope, account uint32, newName string) error {
manager, err := w.Manager.FetchScopedKeyManager(scope) manager, err := w.Manager.FetchScopedKeyManager(scope)
@ -2178,7 +2226,9 @@ type GetTransactionsResult struct {
// Transaction results are organized by blocks in ascending order and unmined // Transaction results are organized by blocks in ascending order and unmined
// transactions in an unspecified order. Mined transactions are saved in a // transactions in an unspecified order. Mined transactions are saved in a
// Block structure which records properties about the block. // Block structure which records properties about the block.
func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, cancel <-chan struct{}) (*GetTransactionsResult, error) { func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier,
accountName string, cancel <-chan struct{}) (*GetTransactionsResult, error) {
var start, end int32 = 0, -1 var start, end int32 = 0, -1
w.chainClientLock.Lock() w.chainClientLock.Lock()
@ -2500,7 +2550,7 @@ func (s creditSlice) Swap(i, j int) {
// contained within it will be considered. If we know nothing about a // contained within it will be considered. If we know nothing about a
// transaction an empty array will be returned. // transaction an empty array will be returned.
func (w *Wallet) ListUnspent(minconf, maxconf int32, func (w *Wallet) ListUnspent(minconf, maxconf int32,
addresses map[string]struct{}) ([]*btcjson.ListUnspentResult, error) { accountName string) ([]*btcjson.ListUnspentResult, error) {
var results []*btcjson.ListUnspentResult var results []*btcjson.ListUnspentResult
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
@ -2509,7 +2559,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
syncBlock := w.Manager.SyncedTo() syncBlock := w.Manager.SyncedTo()
filter := len(addresses) != 0 filter := accountName != ""
unspent, err := w.TxStore.UnspentOutputs(txmgrNs) unspent, err := w.TxStore.UnspentOutputs(txmgrNs)
if err != nil { if err != nil {
return err return err
@ -2548,7 +2598,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
// //
// This will be unnecessary once transactions and outputs are // This will be unnecessary once transactions and outputs are
// grouped under the associated account in the db. // grouped under the associated account in the db.
acctName := defaultAccountName outputAcctName := defaultAccountName
sc, addrs, _, err := txscript.ExtractPkScriptAddrs( sc, addrs, _, err := txscript.ExtractPkScriptAddrs(
output.PkScript, w.chainParams) output.PkScript, w.chainParams)
if err != nil { if err != nil {
@ -2559,22 +2609,15 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
if err == nil { if err == nil {
s, err := smgr.AccountName(addrmgrNs, acct) s, err := smgr.AccountName(addrmgrNs, acct)
if err == nil { if err == nil {
acctName = s outputAcctName = s
} }
} }
} }
if filter { if filter && outputAcctName != accountName {
for _, addr := range addrs {
_, ok := addresses[addr.EncodeAddress()]
if ok {
goto include
}
}
continue continue
} }
include:
// At the moment watch-only addresses are not supported, so all // At the moment watch-only addresses are not supported, so all
// recorded outputs that are not multisig are "spendable". // recorded outputs that are not multisig are "spendable".
// Multisig outputs are only "spendable" if all keys are // Multisig outputs are only "spendable" if all keys are
@ -2614,7 +2657,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
result := &btcjson.ListUnspentResult{ result := &btcjson.ListUnspentResult{
TxID: output.OutPoint.Hash.String(), TxID: output.OutPoint.Hash.String(),
Vout: output.OutPoint.Index, Vout: output.OutPoint.Index,
Account: acctName, Account: outputAcctName,
ScriptPubKey: hex.EncodeToString(output.PkScript), ScriptPubKey: hex.EncodeToString(output.PkScript),
Amount: output.Amount.ToBTC(), Amount: output.Amount.ToBTC(),
Confirmations: int64(confs), Confirmations: int64(confs),
@ -3104,10 +3147,16 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu
return amount, err return amount, err
} }
// SendOutputs creates and sends payment transactions. It returns the // SendOutputs creates and sends payment transactions. Coin selection is
// transaction upon success. // performed by the wallet, choosing inputs that belong to the given key scope
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32, // and account, unless a key scope is not specified. In that case, inputs from
minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error) { // accounts matching the account number provided across all key scopes may be
// selected. This is done to handle the default account case, where a user wants
// to fund a PSBT with inputs regardless of their type (NP2WKH, P2WKH, etc.). It
// returns the transaction upon success.
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
account uint32, minconf int32, satPerKb btcutil.Amount,
label string) (*wire.MsgTx, error) {
// Ensure the outputs to be created adhere to the network's consensus // Ensure the outputs to be created adhere to the network's consensus
// rules. // rules.
@ -3125,7 +3174,7 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32,
// continue to re-broadcast the transaction upon restarts until it has // continue to re-broadcast the transaction upon restarts until it has
// been confirmed. // been confirmed.
createdTx, err := w.CreateSimpleTx( createdTx, err := w.CreateSimpleTx(
account, outputs, minconf, satPerKb, false, keyScope, account, outputs, minconf, satPerKb, false,
) )
if err != nil { if err != nil {
return nil, err return nil, err