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:
commit
e0607006dc
20 changed files with 568 additions and 221 deletions
|
@ -11,4 +11,3 @@ script:
|
||||||
- make lint
|
- make lint
|
||||||
- make unit-race
|
- make unit-race
|
||||||
- make unit-cover
|
- make unit-cover
|
||||||
- make goveralls
|
|
||||||
|
|
11
Makefile
11
Makefile
|
@ -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
|
||||||
|
|
|
@ -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
1
go.mod
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
117
wallet/signer.go
117
wallet/signer.go
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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, "+
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
109
wallet/wallet.go
109
wallet/wallet.go
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue