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 unit-race
|
||||
- make unit-cover
|
||||
- make goveralls
|
||||
|
|
11
Makefile
11
Makefile
|
@ -1,12 +1,10 @@
|
|||
PKG := github.com/btcsuite/btcwallet
|
||||
|
||||
GOVERALLS_PKG := github.com/mattn/goveralls
|
||||
LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
GOACC_PKG := github.com/ory/go-acc
|
||||
GOIMPORTS_PKG := golang.org/x/tools/cmd/goimports
|
||||
|
||||
GO_BIN := ${GOPATH}/bin
|
||||
GOVERALLS_BIN := $(GO_BIN)/goveralls
|
||||
LINT_BIN := $(GO_BIN)/golangci-lint
|
||||
GOACC_BIN := $(GO_BIN)/go-acc
|
||||
|
||||
|
@ -49,10 +47,6 @@ all: build check
|
|||
# DEPENDENCIES
|
||||
# ============
|
||||
|
||||
$(GOVERALLS_BIN):
|
||||
@$(call print, "Fetching goveralls.")
|
||||
go get -u $(GOVERALLS_PKG)
|
||||
|
||||
$(LINT_BIN):
|
||||
@$(call print, "Fetching linter")
|
||||
$(DEPGET) $(LINT_PKG)@$(LINT_COMMIT)
|
||||
|
@ -97,10 +91,6 @@ unit-race:
|
|||
@$(call print, "Running unit race tests.")
|
||||
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
|
||||
# =========
|
||||
|
@ -126,7 +116,6 @@ clean:
|
|||
unit \
|
||||
unit-cover \
|
||||
unit-race \
|
||||
goveralls \
|
||||
fmt \
|
||||
lint \
|
||||
clean
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/btcsuite/btcwallet/netparams"
|
||||
"github.com/btcsuite/btcwallet/wallet/txauthor"
|
||||
"github.com/btcsuite/btcwallet/wallet/txrules"
|
||||
"github.com/btcsuite/btcwallet/wallet/txsizes"
|
||||
"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
|
||||
// all correlated previous input value. A non-change address is created by this
|
||||
// function.
|
||||
func makeDestinationScriptSource(rpcClient *rpcclient.Client, accountName string) txauthor.ChangeSource {
|
||||
return func() ([]byte, error) {
|
||||
func makeDestinationScriptSource(rpcClient *rpcclient.Client, accountName string) *txauthor.ChangeSource {
|
||||
|
||||
// GetNewAddress always returns a P2PKH address since it assumes
|
||||
// BIP-0044.
|
||||
newChangeScript := func() ([]byte, error) {
|
||||
destinationAddress, err := rpcClient.GetNewAddress(accountName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return txscript.PayToAddrScript(destinationAddress)
|
||||
}
|
||||
|
||||
return &txauthor.ChangeSource{
|
||||
ScriptSize: txsizes.P2PKHPkScriptSize,
|
||||
NewScript: newChangeScript,
|
||||
}
|
||||
}
|
||||
|
||||
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/btcwallet/wallet/txauthor 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/wtxmgr v1.2.0
|
||||
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) {
|
||||
cmd := icmd.(*btcjson.ListUnspentCmd)
|
||||
|
||||
var addresses map[string]struct{}
|
||||
if cmd.Addresses != nil {
|
||||
addresses = make(map[string]struct{})
|
||||
// confirm that all of them are good:
|
||||
for _, as := range *cmd.Addresses {
|
||||
a, err := decodeAddress(as, w.ChainParams())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addresses[a.EncodeAddress()] = struct{}{}
|
||||
if cmd.Addresses != nil && len(*cmd.Addresses) > 0 {
|
||||
return nil, &btcjson.RPCError{
|
||||
Code: btcjson.ErrRPCInvalidParameter,
|
||||
Message: "Filtering by addresses has been deprecated",
|
||||
}
|
||||
}
|
||||
|
||||
return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), addresses)
|
||||
return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), "")
|
||||
}
|
||||
|
||||
// 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
|
||||
// All errors are returned in btcjson.RPCError format
|
||||
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())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tx, err := w.SendOutputs(outputs, account, minconf, feeSatPerKb, "")
|
||||
tx, err := w.SendOutputs(outputs, &keyScope, account, minconf, feeSatPerKb, "")
|
||||
if err != nil {
|
||||
if err == txrules.ErrAmountNegative {
|
||||
return "", ErrNeedPositiveAmount
|
||||
|
@ -1433,7 +1428,7 @@ func sendFrom(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient)
|
|||
cmd.ToAddress: amt,
|
||||
}
|
||||
|
||||
return sendPairs(w, pairs, account, minConf,
|
||||
return sendPairs(w, pairs, waddrmgr.KeyScopeBIP0044, account, minConf,
|
||||
txrules.DefaultRelayFeePerKb)
|
||||
}
|
||||
|
||||
|
@ -1475,7 +1470,7 @@ func sendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
|||
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
|
||||
|
@ -1511,7 +1506,7 @@ func sendToAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -408,7 +408,7 @@ func (s *walletServer) GetTransactions(ctx context.Context, req *pb.GetTransacti
|
|||
|
||||
_ = minRecentTxs
|
||||
|
||||
gtr, err := s.wallet.GetTransactions(startBlock, endBlock, ctx.Done())
|
||||
gtr, err := s.wallet.GetTransactions(startBlock, endBlock, "", ctx.Done())
|
||||
if err != nil {
|
||||
return nil, translateError(err)
|
||||
}
|
||||
|
|
|
@ -405,6 +405,29 @@ func (m *Manager) watchOnly() bool {
|
|||
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
|
||||
// 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
|
||||
// them being created while the address manager was locked.
|
||||
for _, info := range manager.deriveOnUnlock {
|
||||
addressKey, _, err := manager.deriveKeyFromPath(
|
||||
addressKey, _, _, err := manager.deriveKeyFromPath(
|
||||
ns, info.managedAddr.InternalAccount(),
|
||||
info.branch, info.index, true,
|
||||
)
|
||||
|
@ -1276,6 +1299,25 @@ func ValidateAccountName(name string) error {
|
|||
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
|
||||
// error is returned when an invalid key type is specified or the requested key
|
||||
// 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
|
||||
// key index within as a child of the account and branch.
|
||||
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
|
||||
|
@ -444,10 +450,11 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
|
|||
index--
|
||||
}
|
||||
lastExtAddrPath := DerivationPath{
|
||||
InternalAccount: account,
|
||||
Account: acctInfo.acctKeyPub.ChildIndex(),
|
||||
Branch: branch,
|
||||
Index: index,
|
||||
InternalAccount: account,
|
||||
Account: acctInfo.acctKeyPub.ChildIndex(),
|
||||
Branch: branch,
|
||||
Index: index,
|
||||
MasterKeyFingerprint: acctInfo.masterKeyFingerprint,
|
||||
}
|
||||
lastExtKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey)
|
||||
if err != nil {
|
||||
|
@ -465,10 +472,11 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
|
|||
index--
|
||||
}
|
||||
lastIntAddrPath := DerivationPath{
|
||||
InternalAccount: account,
|
||||
Account: acctInfo.acctKeyPub.ChildIndex(),
|
||||
Branch: branch,
|
||||
Index: index,
|
||||
InternalAccount: account,
|
||||
Account: acctInfo.acctKeyPub.ChildIndex(),
|
||||
Branch: branch,
|
||||
Index: index,
|
||||
MasterKeyFingerprint: acctInfo.masterKeyFingerprint,
|
||||
}
|
||||
lastIntKey, err := s.deriveKey(acctInfo, branch, index, hasPrivateKey)
|
||||
if err != nil {
|
||||
|
@ -580,7 +588,7 @@ func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket,
|
|||
watchOnly := s.rootManager.WatchOnly()
|
||||
private := !s.rootManager.IsLocked() && !watchOnly
|
||||
|
||||
addrKey, _, err := s.deriveKeyFromPath(
|
||||
addrKey, _, _, err := s.deriveKeyFromPath(
|
||||
ns, kp.InternalAccount, kp.Branch, kp.Index, private,
|
||||
)
|
||||
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.
|
||||
func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket,
|
||||
internalAccount, branch, index uint32, private bool) (
|
||||
*hdkeychain.ExtendedKey, *hdkeychain.ExtendedKey, error) {
|
||||
*hdkeychain.ExtendedKey, *hdkeychain.ExtendedKey, uint32, error) {
|
||||
|
||||
// Look up the account key information.
|
||||
acctInfo, err := s.loadAccountInfo(ns, internalAccount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
private = private && acctInfo.acctKeyPriv != nil
|
||||
|
||||
addrKey, err := s.deriveKey(acctInfo, branch, index, private)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
acctKey := acctInfo.acctKeyPub
|
||||
|
@ -620,7 +628,7 @@ func (s *ScopedKeyManager) deriveKeyFromPath(ns walletdb.ReadBucket,
|
|||
acctKey = acctInfo.acctKeyPriv
|
||||
}
|
||||
|
||||
return addrKey, acctKey, nil
|
||||
return addrKey, acctKey, acctInfo.masterKeyFingerprint, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
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,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -647,10 +655,11 @@ func (s *ScopedKeyManager) chainAddressRowToManaged(ns walletdb.ReadBucket,
|
|||
}
|
||||
return s.keyToManaged(
|
||||
addressKey, DerivationPath{
|
||||
InternalAccount: row.account,
|
||||
Account: acctKey.ChildIndex(),
|
||||
Branch: row.branch,
|
||||
Index: row.index,
|
||||
InternalAccount: row.account,
|
||||
Account: acctKey.ChildIndex(),
|
||||
Branch: row.branch,
|
||||
Index: row.index,
|
||||
MasterKeyFingerprint: masterKeyFingerprint,
|
||||
}, acctInfo,
|
||||
)
|
||||
}
|
||||
|
@ -2177,6 +2186,22 @@ func (s *ScopedKeyManager) ForEachInternalActiveAddress(ns walletdb.ReadBucket,
|
|||
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
|
||||
// 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
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/btcsuite/btcwallet/wallet/txauthor"
|
||||
"github.com/btcsuite/btcwallet/wallet/txsizes"
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
"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
|
||||
// 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
|
||||
// change to the wallet. An appropriate fee is included based on the wallet's
|
||||
// current relay fee. The wallet must be unlocked to create the transaction.
|
||||
// change to the wallet. This output will have an address generated from the
|
||||
// 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
|
||||
// the database. A tx created with this set to true will intentionally have no
|
||||
// input scripts added and SHOULD NOT be broadcasted.
|
||||
func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
|
||||
minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) (
|
||||
func (w *Wallet) txToOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
|
||||
account uint32, minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) (
|
||||
tx *txauthor.AuthoredTx, err error) {
|
||||
|
||||
chainClient, err := w.requireChainClient()
|
||||
|
@ -120,7 +124,12 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
|
|||
}
|
||||
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.
|
||||
bs, err := chainClient.BlockStamp()
|
||||
|
@ -128,14 +137,17 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs)
|
||||
eligible, err := w.findEligibleOutputs(
|
||||
dbtx, keyScope, account, minconf, bs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputSource := makeInputSource(eligible)
|
||||
tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb,
|
||||
inputSource, changeSource)
|
||||
tx, err = txauthor.NewUnsignedTransaction(
|
||||
outputs, feeSatPerKb, inputSource, changeSource,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -155,14 +167,36 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
|
|||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
err = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := dbtx.Commit(); err != nil {
|
||||
|
@ -193,7 +227,10 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32,
|
|||
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)
|
||||
txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
|
||||
|
||||
|
@ -239,8 +276,14 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, account uint32, minco
|
|||
if err != nil || len(addrs) != 1 {
|
||||
continue
|
||||
}
|
||||
_, addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0])
|
||||
if err != nil || addrAcct != account {
|
||||
scopedMgr, addrAcct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if keyScope != nil && scopedMgr.Scope() != *keyScope {
|
||||
continue
|
||||
}
|
||||
if addrAcct != account {
|
||||
continue
|
||||
}
|
||||
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
|
||||
// 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,
|
||||
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)
|
||||
changeSource := func() ([]byte, error) {
|
||||
// Derive the change output script. We'll use the default key
|
||||
// scope responsible for P2WPKH addresses to do so. As a hack to
|
||||
// allow spending from the imported account, change addresses
|
||||
// are created from account 0.
|
||||
var changeAddr btcutil.Address
|
||||
var err error
|
||||
changeKeyScope := waddrmgr.KeyScopeBIP0084
|
||||
scopeMgr, err := w.Manager.FetchScopedKeyManager(*changeKeyScope)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
accountInfo, err := scopeMgr.AccountProperties(addrmgrNs, account)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
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 {
|
||||
changeAddr, err = w.newChangeAddress(
|
||||
addrmgrNs, 0, changeKeyScope,
|
||||
addrmgrNs, 0, *changeKeyScope,
|
||||
)
|
||||
} else {
|
||||
changeAddr, err = w.newChangeAddress(
|
||||
addrmgrNs, account, changeKeyScope,
|
||||
addrmgrNs, account, *changeKeyScope,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -276,7 +354,11 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx,
|
|||
}
|
||||
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
|
||||
|
|
|
@ -34,7 +34,8 @@ func TestTxToOutputsDryRun(t *testing.T) {
|
|||
defer cleanup()
|
||||
|
||||
// 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 {
|
||||
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
|
||||
// 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 {
|
||||
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))
|
||||
}
|
||||
|
||||
dryRunTx2, err := w.txToOutputs(txOuts, 0, 1, 1000, true)
|
||||
dryRunTx2, err := w.txToOutputs(txOuts, nil, 0, 1, 1000, true)
|
||||
if err != nil {
|
||||
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
|
||||
// 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 {
|
||||
t.Fatalf("unable to author tx: %v", err)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,10 @@ import (
|
|||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/btcsuite/btcwallet/wallet/txauthor"
|
||||
"github.com/btcsuite/btcwallet/wallet/txrules"
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||
)
|
||||
|
||||
|
@ -24,17 +26,22 @@ import (
|
|||
// is created and the index -1 is returned.
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
// automatically, only selecting inputs from the account based on the given key
|
||||
// scope and account number. If a key scope is not specified, then inputs from
|
||||
// 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.). 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
|
||||
// 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
|
||||
// responsibility to lock the inputs before handing the partial transaction out.
|
||||
func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
|
||||
feeSatPerKB btcutil.Amount) (int32, error) {
|
||||
func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
|
||||
account uint32, feeSatPerKB btcutil.Amount) (int32, error) {
|
||||
|
||||
// Make sure the packet is well formed. We only require there to be at
|
||||
// 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 {
|
||||
packet.Inputs = make([]psbt.PInput, len(inputs))
|
||||
for idx, in := range inputs {
|
||||
tx, utxo, _, err := w.FetchInputInfo(
|
||||
tx, utxo, derivationPath, _, err := w.FetchInputInfo(
|
||||
&in.PreviousOutPoint,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -91,10 +98,27 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
|
|||
}
|
||||
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
|
||||
// just yet.
|
||||
// on the unsigned TX just yet.
|
||||
packet.UnsignedTx.TxIn[idx].Witness = wire.TxWitness{}
|
||||
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
|
||||
|
@ -108,8 +132,8 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, account uint32,
|
|||
// includes everything we need, specifically fee estimation and
|
||||
// change address creation.
|
||||
tx, err = w.CreateSimpleTx(
|
||||
account, packet.UnsignedTx.TxOut, 1, feeSatPerKB,
|
||||
false,
|
||||
keyScope, account, packet.UnsignedTx.TxOut, 1,
|
||||
feeSatPerKB, false,
|
||||
)
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
// We need at least one input and one output.
|
||||
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
|
||||
// to a coin we own. If we can't, then we'll continue as it
|
||||
// isn't our input.
|
||||
fullTx, txOut, _, err := w.FetchInputInfo(
|
||||
fullTx, txOut, _, _, err := w.FetchInputInfo(
|
||||
&txIn.PreviousOutPoint,
|
||||
)
|
||||
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
|
||||
// with the witness and sigScript (if needed).
|
||||
// Finally, if the input doesn't belong to a watch-only account,
|
||||
// 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(
|
||||
tx, signOutput, idx, sigHashes, in.SighashType, nil,
|
||||
)
|
||||
|
|
|
@ -219,7 +219,7 @@ func TestFundPsbt(t *testing.T) {
|
|||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
changeIndex, err := w.FundPsbt(
|
||||
tc.packet, 0, tc.feeRateSatPerKB,
|
||||
tc.packet, nil, 0, tc.feeRateSatPerKB,
|
||||
)
|
||||
|
||||
// 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.
|
||||
err = w.FinalizePsbt(packet)
|
||||
err = w.FinalizePsbt(nil, 0, packet)
|
||||
if err != nil {
|
||||
t.Fatalf("error finalizing PSBT packet: %v", err)
|
||||
}
|
||||
|
|
117
wallet/signer.go
117
wallet/signer.go
|
@ -5,6 +5,8 @@
|
|||
package wallet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
|
@ -12,6 +14,71 @@ import (
|
|||
"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
|
||||
// tweaking a private key before it's used to sign an input.
|
||||
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,
|
||||
[]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)
|
||||
walletAddr, witnessProgram, sigScript, err := w.scriptForOutput(output)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pka := walletAddr.(waddrmgr.ManagedPubKeyAddress)
|
||||
privKey, err := pka.PrivKey()
|
||||
privKey, err := walletAddr.PrivKey()
|
||||
if err != nil {
|
||||
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 tweaker != nil {
|
||||
privKey, err = tweaker(privKey)
|
||||
|
|
|
@ -60,8 +60,15 @@ type AuthoredTx struct {
|
|||
ChangeIndex int // negative if no change
|
||||
}
|
||||
|
||||
// ChangeSource provides P2PKH change output scripts for transaction creation.
|
||||
type ChangeSource func() ([]byte, error)
|
||||
// ChangeSource provides change output scripts for transaction creation.
|
||||
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
|
||||
// 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.
|
||||
func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount,
|
||||
fetchInputs InputSource, fetchChange ChangeSource) (*AuthoredTx, error) {
|
||||
fetchInputs InputSource, changeSource *ChangeSource) (*AuthoredTx, error) {
|
||||
|
||||
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)
|
||||
|
||||
for {
|
||||
|
@ -115,8 +124,9 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount,
|
|||
}
|
||||
}
|
||||
|
||||
maxSignedSize := txsizes.EstimateVirtualSize(p2pkh, p2wpkh,
|
||||
nested, outputs, true)
|
||||
maxSignedSize := txsizes.EstimateVirtualSize(
|
||||
p2pkh, p2wpkh, nested, outputs, changeSource.ScriptSize,
|
||||
)
|
||||
maxRequiredFee := txrules.FeeForSerializeSize(feeRatePerKb, maxSignedSize)
|
||||
remainingAmount := inputAmount - targetAmount
|
||||
if remainingAmount < maxRequiredFee {
|
||||
|
@ -130,18 +140,16 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount,
|
|||
TxOut: outputs,
|
||||
LockTime: 0,
|
||||
}
|
||||
|
||||
changeIndex := -1
|
||||
changeAmount := inputAmount - targetAmount - maxRequiredFee
|
||||
if changeAmount != 0 && !txrules.IsDustAmount(changeAmount,
|
||||
txsizes.P2WPKHPkScriptSize, txrules.DefaultRelayFeePerKb) {
|
||||
changeScript, err := fetchChange()
|
||||
changeSource.ScriptSize, txrules.DefaultRelayFeePerKb) {
|
||||
|
||||
changeScript, err := changeSource.NewScript()
|
||||
if err != nil {
|
||||
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)
|
||||
l := len(outputs)
|
||||
unsignedTransaction.TxOut = append(outputs[:l:l], change)
|
||||
|
|
|
@ -61,7 +61,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
Outputs: p2pkhOutputs(1e6),
|
||||
RelayFee: 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,
|
||||
},
|
||||
2: {
|
||||
|
@ -69,7 +69,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
Outputs: p2pkhOutputs(1e6),
|
||||
RelayFee: 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,
|
||||
},
|
||||
3: {
|
||||
|
@ -77,7 +77,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
Outputs: p2pkhOutputs(1e6, 1e6, 1e6),
|
||||
RelayFee: 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,
|
||||
},
|
||||
4: {
|
||||
|
@ -85,7 +85,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
Outputs: p2pkhOutputs(1e6, 1e6, 1e6),
|
||||
RelayFee: 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,
|
||||
},
|
||||
|
||||
|
@ -93,7 +93,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
5: {
|
||||
UnspentOutputs: p2pkhOutputs(1e8),
|
||||
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,
|
||||
ChangeAmount: 545,
|
||||
InputCount: 1,
|
||||
|
@ -101,7 +101,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
6: {
|
||||
UnspentOutputs: p2pkhOutputs(1e8),
|
||||
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,
|
||||
ChangeAmount: 546,
|
||||
InputCount: 1,
|
||||
|
@ -111,7 +111,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
7: {
|
||||
UnspentOutputs: p2pkhOutputs(1e8),
|
||||
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,
|
||||
ChangeAmount: 1392,
|
||||
InputCount: 1,
|
||||
|
@ -119,7 +119,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
8: {
|
||||
UnspentOutputs: p2pkhOutputs(1e8),
|
||||
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,
|
||||
ChangeAmount: 1393,
|
||||
InputCount: 1,
|
||||
|
@ -131,7 +131,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
9: {
|
||||
UnspentOutputs: p2pkhOutputs(1e8, 1e8),
|
||||
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,
|
||||
ChangeAmount: 546,
|
||||
InputCount: 1,
|
||||
|
@ -145,7 +145,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
10: {
|
||||
UnspentOutputs: p2pkhOutputs(1e8, 1e8),
|
||||
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,
|
||||
ChangeAmount: 545,
|
||||
InputCount: 1,
|
||||
|
@ -157,7 +157,7 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
Outputs: p2pkhOutputs(1e8),
|
||||
RelayFee: 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,
|
||||
},
|
||||
|
||||
|
@ -172,9 +172,12 @@ func TestNewUnsignedTransaction(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
changeSource := func() ([]byte, error) {
|
||||
// Only length matters for these tests.
|
||||
return make([]byte, txsizes.P2WPKHPkScriptSize), nil
|
||||
changeSource := &ChangeSource{
|
||||
NewScript: func() ([]byte, error) {
|
||||
// Only length matters for these tests.
|
||||
return make([]byte, txsizes.P2WPKHPkScriptSize), nil
|
||||
},
|
||||
ScriptSize: txsizes.P2WPKHPkScriptSize,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
|
|
@ -82,6 +82,16 @@ const (
|
|||
// - 4 bytes sequence
|
||||
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
|
||||
// input script that redeems a pay-to-witness-key hash nested in P2SH
|
||||
// (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
|
||||
// change output if addChangeOutput is true.
|
||||
func EstimateVirtualSize(numP2PKHIns, numP2WPKHIns, numNestedP2WPKHIns int,
|
||||
txOuts []*wire.TxOut, addChangeOutput bool) int {
|
||||
changeSize := 0
|
||||
txOuts []*wire.TxOut, changeScriptSize int) int {
|
||||
outputCount := len(txOuts)
|
||||
if addChangeOutput {
|
||||
// We are always using P2WPKH as change output.
|
||||
changeSize = P2WPKHOutputSize
|
||||
|
||||
changeOutputSize := 0
|
||||
if changeScriptSize > 0 {
|
||||
changeOutputSize = 8 +
|
||||
wire.VarIntSerializeSize(uint64(changeScriptSize)) +
|
||||
changeScriptSize
|
||||
outputCount++
|
||||
}
|
||||
|
||||
|
@ -170,7 +182,7 @@ func EstimateVirtualSize(numP2PKHIns, numP2WPKHIns, numNestedP2WPKHIns int,
|
|||
numP2WPKHIns*RedeemP2WPKHInputSize +
|
||||
numNestedP2WPKHIns*RedeemNestedP2WPKHInputSize +
|
||||
SumOutputSerializeSizes(txOuts) +
|
||||
changeSize
|
||||
changeOutputSize
|
||||
|
||||
// If this transaction has any witness inputs, we must count the
|
||||
// witness data.
|
||||
|
|
|
@ -163,8 +163,12 @@ func TestEstimateVirtualSize(t *testing.T) {
|
|||
t.Fatalf("unable to get test tx: %v", err)
|
||||
}
|
||||
|
||||
changeScriptSize := 0
|
||||
if test.change {
|
||||
changeScriptSize = P2WPKHPkScriptSize
|
||||
}
|
||||
est := EstimateVirtualSize(test.p2pkhIns, test.p2wpkhIns,
|
||||
test.nestedp2wpkhIns, tx.TxOut, test.change)
|
||||
test.nestedp2wpkhIns, tx.TxOut, changeScriptSize)
|
||||
|
||||
if est != test.result {
|
||||
t.Fatalf("expected estimated vsize to be %d, "+
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"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
|
||||
// returned. Otherwise, a non-nil error value of ErrNotMine is returned instead.
|
||||
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.
|
||||
txid := &prevOut.Hash
|
||||
txDetail, err := UnstableAPI(w).TxDetails(txid)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
return nil, nil, nil, 0, err
|
||||
} 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
|
||||
|
@ -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.
|
||||
numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut))
|
||||
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,
|
||||
numOutputs)
|
||||
}
|
||||
pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript
|
||||
if _, err := w.fetchOutputAddr(pkScript); err != nil {
|
||||
return nil, nil, 0, err
|
||||
addr, err := w.fetchOutputAddr(pkScript)
|
||||
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.
|
||||
_, currentHeight, err := w.chainClient.GetBestBlock()
|
||||
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)
|
||||
}
|
||||
confs := int64(0)
|
||||
|
@ -143,9 +151,19 @@ func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx,
|
|||
}
|
||||
|
||||
return &txDetail.TxRecord.MsgTx, &wire.TxOut{
|
||||
Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value,
|
||||
PkScript: pkScript,
|
||||
}, confs, nil
|
||||
Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value,
|
||||
PkScript: pkScript,
|
||||
}, &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
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
)
|
||||
|
||||
|
@ -43,7 +44,7 @@ func TestFetchInputInfo(t *testing.T) {
|
|||
Hash: incomingTx.TxHash(),
|
||||
Index: 0,
|
||||
}
|
||||
tx, out, confirmations, err := w.FetchInputInfo(prevOut)
|
||||
tx, out, derivationPath, confirmations, err := w.FetchInputInfo(prevOut)
|
||||
if err != nil {
|
||||
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",
|
||||
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) {
|
||||
t.Fatalf("unexpected number of confirmations, got %d wanted %d",
|
||||
confirmations, 0-testBlockHeight)
|
||||
|
|
109
wallet/wallet.go
109
wallet/wallet.go
|
@ -1125,6 +1125,7 @@ func logFilterBlocksResp(block wtxmgr.BlockMeta,
|
|||
|
||||
type (
|
||||
createTxRequest struct {
|
||||
keyScope *waddrmgr.KeyScope
|
||||
account uint32
|
||||
outputs []*wire.TxOut
|
||||
minconf int32
|
||||
|
@ -1159,8 +1160,10 @@ out:
|
|||
txr.resp <- createTxResponse{nil, err}
|
||||
continue
|
||||
}
|
||||
tx, err := w.txToOutputs(txr.outputs, txr.account,
|
||||
txr.minconf, txr.feeSatPerKB, txr.dryRun)
|
||||
tx, err := w.txToOutputs(
|
||||
txr.outputs, txr.keyScope, txr.account,
|
||||
txr.minconf, txr.feeSatPerKB, txr.dryRun,
|
||||
)
|
||||
heldUnlock.release()
|
||||
txr.resp <- createTxResponse{tx, err}
|
||||
case <-quit:
|
||||
|
@ -1170,20 +1173,25 @@ out:
|
|||
w.wg.Done()
|
||||
}
|
||||
|
||||
// CreateSimpleTx creates a new signed transaction spending unspent P2PKH
|
||||
// outputs with at least minconf confirmations spending to any number of
|
||||
// address/amount pairs. Change and an appropriate transaction fee are
|
||||
// automatically included, if necessary. All transaction creation through this
|
||||
// function is serialized to prevent the creation of many transactions which
|
||||
// spend the same outputs.
|
||||
// CreateSimpleTx creates a new signed transaction spending unspent outputs with
|
||||
// at least minconf confirmations spending to any number of address/amount
|
||||
// pairs. Only unspent outputs belonging to the given key scope and account will
|
||||
// be selected, unless a key scope is not specified. In that case, inputs from all
|
||||
// accounts may be selected, no matter what key scope they belong to. 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.). 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
|
||||
// the database. A tx created with this set to true SHOULD NOT be broadcasted.
|
||||
func (w *Wallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut,
|
||||
minconf int32, satPerKb btcutil.Amount, dryRun bool) (
|
||||
*txauthor.AuthoredTx, error) {
|
||||
func (w *Wallet) CreateSimpleTx(keyScope *waddrmgr.KeyScope, account uint32,
|
||||
outputs []*wire.TxOut, minconf int32, satPerKb btcutil.Amount,
|
||||
dryRun bool) (*txauthor.AuthoredTx, error) {
|
||||
|
||||
req := createTxRequest{
|
||||
keyScope: keyScope,
|
||||
account: account,
|
||||
outputs: outputs,
|
||||
minconf: minconf,
|
||||
|
@ -1754,6 +1762,46 @@ func (w *Wallet) AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddr
|
|||
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.
|
||||
func (w *Wallet) RenameAccount(scope waddrmgr.KeyScope, account uint32, newName string) error {
|
||||
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
||||
|
@ -2178,7 +2226,9 @@ type GetTransactionsResult struct {
|
|||
// Transaction results are organized by blocks in ascending order and unmined
|
||||
// transactions in an unspecified order. Mined transactions are saved in a
|
||||
// Block structure which records properties about the block.
|
||||
func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, cancel <-chan struct{}) (*GetTransactionsResult, error) {
|
||||
func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier,
|
||||
accountName string, cancel <-chan struct{}) (*GetTransactionsResult, error) {
|
||||
|
||||
var start, end int32 = 0, -1
|
||||
|
||||
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
|
||||
// transaction an empty array will be returned.
|
||||
func (w *Wallet) ListUnspent(minconf, maxconf int32,
|
||||
addresses map[string]struct{}) ([]*btcjson.ListUnspentResult, error) {
|
||||
accountName string) ([]*btcjson.ListUnspentResult, error) {
|
||||
|
||||
var results []*btcjson.ListUnspentResult
|
||||
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()
|
||||
|
||||
filter := len(addresses) != 0
|
||||
filter := accountName != ""
|
||||
unspent, err := w.TxStore.UnspentOutputs(txmgrNs)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2548,7 +2598,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
|
|||
//
|
||||
// This will be unnecessary once transactions and outputs are
|
||||
// grouped under the associated account in the db.
|
||||
acctName := defaultAccountName
|
||||
outputAcctName := defaultAccountName
|
||||
sc, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
||||
output.PkScript, w.chainParams)
|
||||
if err != nil {
|
||||
|
@ -2559,22 +2609,15 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
|
|||
if err == nil {
|
||||
s, err := smgr.AccountName(addrmgrNs, acct)
|
||||
if err == nil {
|
||||
acctName = s
|
||||
outputAcctName = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if filter {
|
||||
for _, addr := range addrs {
|
||||
_, ok := addresses[addr.EncodeAddress()]
|
||||
if ok {
|
||||
goto include
|
||||
}
|
||||
}
|
||||
if filter && outputAcctName != accountName {
|
||||
continue
|
||||
}
|
||||
|
||||
include:
|
||||
// At the moment watch-only addresses are not supported, so all
|
||||
// recorded outputs that are not multisig are "spendable".
|
||||
// Multisig outputs are only "spendable" if all keys are
|
||||
|
@ -2614,7 +2657,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32,
|
|||
result := &btcjson.ListUnspentResult{
|
||||
TxID: output.OutPoint.Hash.String(),
|
||||
Vout: output.OutPoint.Index,
|
||||
Account: acctName,
|
||||
Account: outputAcctName,
|
||||
ScriptPubKey: hex.EncodeToString(output.PkScript),
|
||||
Amount: output.Amount.ToBTC(),
|
||||
Confirmations: int64(confs),
|
||||
|
@ -3104,10 +3147,16 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu
|
|||
return amount, err
|
||||
}
|
||||
|
||||
// SendOutputs creates and sends payment transactions. It returns the
|
||||
// transaction upon success.
|
||||
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32,
|
||||
minconf int32, satPerKb btcutil.Amount, label string) (*wire.MsgTx, error) {
|
||||
// SendOutputs creates and sends payment transactions. Coin selection is
|
||||
// performed by the wallet, choosing inputs that belong to the given key scope
|
||||
// and account, unless a key scope is not specified. In that case, inputs from
|
||||
// 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
|
||||
// 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
|
||||
// been confirmed.
|
||||
createdTx, err := w.CreateSimpleTx(
|
||||
account, outputs, minconf, satPerKb, false,
|
||||
keyScope, account, outputs, minconf, satPerKb, false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
Loading…
Reference in a new issue