Merge pull request #732 from wpaulino/import-account-or-pubkey
wallet: support derived public key import
This commit is contained in:
commit
5c08d49e19
18 changed files with 1748 additions and 465 deletions
|
@ -1,6 +1,6 @@
|
|||
language: go
|
||||
go:
|
||||
- "1.13.x"
|
||||
- "1.15.x"
|
||||
sudo: false
|
||||
install:
|
||||
- GO111MODULE=on go install -v ./...
|
||||
|
|
9
go.mod
9
go.mod
|
@ -3,8 +3,8 @@ module github.com/btcsuite/btcwallet
|
|||
require (
|
||||
github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
|
||||
github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0
|
||||
github.com/btcsuite/btcutil 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/txrules v1.0.0
|
||||
github.com/btcsuite/btcwallet/walletdb v1.3.4
|
||||
|
@ -17,8 +17,9 @@ require (
|
|||
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf
|
||||
github.com/lightninglabs/neutrino v0.11.0
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
|
||||
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 // indirect
|
||||
google.golang.org/grpc v1.18.0
|
||||
)
|
||||
|
|
13
go.sum
13
go.sum
|
@ -11,8 +11,10 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9
|
|||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4=
|
||||
github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ=
|
||||
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ=
|
||||
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
|
||||
github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce h1:3PRwz+js0AMMV1fHRrCdQ55akoomx4Q3ulozHC3BDDY=
|
||||
github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
|
@ -85,6 +87,9 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4
|
|||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -95,12 +100,16 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953 h1:LuZIitY8waaxUfNIdtajyE/Yz
|
|||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
|
|
|
@ -1774,7 +1774,9 @@ func validateAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
|
|||
// The address lookup was successful which means there is further
|
||||
// information about it available and it is "mine".
|
||||
result.IsMine = true
|
||||
acctName, err := w.AccountName(waddrmgr.KeyScopeBIP0044, ainfo.Account())
|
||||
acctName, err := w.AccountName(
|
||||
waddrmgr.KeyScopeBIP0044, ainfo.InternalAccount(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, &ErrAccountNameNotFound
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ func TestThrottle(t *testing.T) {
|
|||
go func() {
|
||||
res, err := http.Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
codes <- res.StatusCode
|
||||
}()
|
||||
|
|
|
@ -1210,10 +1210,12 @@ type SpentnessNotificationsResponse struct {
|
|||
Spender *SpentnessNotificationsResponse_Spender `protobuf:"bytes,3,opt,name=spender" json:"spender,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SpentnessNotificationsResponse) Reset() { *m = SpentnessNotificationsResponse{} }
|
||||
func (m *SpentnessNotificationsResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*SpentnessNotificationsResponse) ProtoMessage() {}
|
||||
func (*SpentnessNotificationsResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{36} }
|
||||
func (m *SpentnessNotificationsResponse) Reset() { *m = SpentnessNotificationsResponse{} }
|
||||
func (m *SpentnessNotificationsResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*SpentnessNotificationsResponse) ProtoMessage() {}
|
||||
func (*SpentnessNotificationsResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor0, []int{36}
|
||||
}
|
||||
|
||||
func (m *SpentnessNotificationsResponse) GetTransactionHash() []byte {
|
||||
if m != nil {
|
||||
|
|
|
@ -54,8 +54,8 @@ const (
|
|||
// type may provide further fields to provide information specific to that type
|
||||
// of address.
|
||||
type ManagedAddress interface {
|
||||
// Account returns the account the address is associated with.
|
||||
Account() uint32
|
||||
// Account returns the internal account the address is associated with.
|
||||
InternalAccount() uint32
|
||||
|
||||
// Address returns a btcutil.Address for the backing address.
|
||||
Address() btcutil.Address
|
||||
|
@ -133,7 +133,7 @@ type managedAddress struct {
|
|||
used bool
|
||||
addrType AddressType
|
||||
pubKey *btcec.PublicKey
|
||||
privKeyEncrypted []byte
|
||||
privKeyEncrypted []byte // nil if part of watch-only account
|
||||
privKeyCT []byte // non-nil if unlocked
|
||||
privKeyMutex sync.Mutex
|
||||
}
|
||||
|
@ -151,6 +151,12 @@ func (a *managedAddress) unlock(key EncryptorDecryptor) ([]byte, error) {
|
|||
a.privKeyMutex.Lock()
|
||||
defer a.privKeyMutex.Unlock()
|
||||
|
||||
// If the address belongs to a watch-only account, the encrypted private
|
||||
// key won't be present, so we'll return an error.
|
||||
if len(a.privKeyEncrypted) == 0 {
|
||||
return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil)
|
||||
}
|
||||
|
||||
if len(a.privKeyCT) == 0 {
|
||||
privKey, err := key.Decrypt(a.privKeyEncrypted)
|
||||
if err != nil {
|
||||
|
@ -177,11 +183,12 @@ func (a *managedAddress) lock() {
|
|||
a.privKeyMutex.Unlock()
|
||||
}
|
||||
|
||||
// Account returns the account number the address is associated with.
|
||||
// InternalAccount returns the internal account number the address is associated
|
||||
// with.
|
||||
//
|
||||
// This is part of the ManagedAddress interface implementation.
|
||||
func (a *managedAddress) Account() uint32 {
|
||||
return a.derivationPath.Account
|
||||
func (a *managedAddress) InternalAccount() uint32 {
|
||||
return a.derivationPath.InternalAccount
|
||||
}
|
||||
|
||||
// AddrType returns the address type of the managed address. This can be used
|
||||
|
@ -544,11 +551,11 @@ func (a *scriptAddress) lock() {
|
|||
a.scriptMutex.Unlock()
|
||||
}
|
||||
|
||||
// Account returns the account the address is associated with. This will always
|
||||
// be the ImportedAddrAccount constant for script addresses.
|
||||
// InternalAccount returns the account the address is associated with. This will
|
||||
// always be the ImportedAddrAccount constant for script addresses.
|
||||
//
|
||||
// This is part of the ManagedAddress interface implementation.
|
||||
func (a *scriptAddress) Account() uint32 {
|
||||
func (a *scriptAddress) InternalAccount() uint32 {
|
||||
return a.account
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||
)
|
||||
|
@ -51,9 +52,10 @@ var (
|
|||
privKey: hexToBytes("c27d6581b92785834b381fa697c4b0ffc4574b495743722e0acb7601b1b68b99"),
|
||||
privKeyWIF: "L3jmpy54Pc7MLXTN2mL8Xas7BJziwKaUGmgnXXzgGbVRdiAniXZk",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 0,
|
||||
Index: 0,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 0,
|
||||
Index: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -66,9 +68,10 @@ var (
|
|||
privKey: hexToBytes("18f3b191019e83878a81557abebb2afda199e31d22e150d8bf4df4561671be6c"),
|
||||
privKeyWIF: "Kx4DNid19W8sjNFN3uPqQE7UYnCqyEp7unCvdkf2LrVUFpnDtwpB",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 0,
|
||||
Index: 1,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 0,
|
||||
Index: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -81,9 +84,10 @@ var (
|
|||
privKey: hexToBytes("ccb8f6305b73136b363644b647f6efc0fd27b6b7d9c11c7e560662ed38db7b34"),
|
||||
privKeyWIF: "L45fWF6Yd736fDohuB97vwRRLdQQJr3ZGvbokk9ubiT7aNrg7tTn",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 0,
|
||||
Index: 2,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 0,
|
||||
Index: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -96,9 +100,10 @@ var (
|
|||
privKey: hexToBytes("d6bc8ff768814fede2adcdb74826bd846924341b3862e3b6e31cdc084e992940"),
|
||||
privKeyWIF: "L4R8XyxYQyPSpTwj8w96tM86a6j3QA9jbRPj3RA7DVTVWk71ndeP",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 0,
|
||||
Index: 3,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 0,
|
||||
Index: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -111,9 +116,10 @@ var (
|
|||
privKey: hexToBytes("8563ade061110e03aee50695ffc5cb1c06c8310bde0a3674257c853c966968c0"),
|
||||
privKeyWIF: "L1h16Hunxomww4FrpyQP2iFmWNgG7U1u3awp6Vd3s2uGf7v5VU8c",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 0,
|
||||
Index: 4,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 0,
|
||||
Index: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -126,9 +132,10 @@ var (
|
|||
privKey: hexToBytes("fe4f855fcf059ec6ddf7b25f63b19aa49c771d1fcb9850b68ae3d65e20657a60"),
|
||||
privKeyWIF: "L5k4HivqXvohxBMpuwD38iUgi6uewffwZny91ZNYfM39RXH2x3QR",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 1,
|
||||
Index: 0,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 1,
|
||||
Index: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -141,9 +148,10 @@ var (
|
|||
privKey: hexToBytes("bfef521317c65b018ae7e6d7ecc3aa700d5d0f7ea84d567be9270382d0b5e3e6"),
|
||||
privKeyWIF: "L3eomUajnTDM3Pc8GU47qqXUFuCjvpqY7NYN9mH3x1ZFjDgiY4BU",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 1,
|
||||
Index: 1,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 1,
|
||||
Index: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -156,9 +164,10 @@ var (
|
|||
privKey: hexToBytes("f506dffd4494c24006df7a35f3291f7ca0297a1a431557a1339bfed6f48738ca"),
|
||||
privKeyWIF: "L5S1bVQUPqQb1Su82fLoSpnGCjcPfdAQE1pJxWRopJSBdYNDHESv",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 1,
|
||||
Index: 2,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 1,
|
||||
Index: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -171,9 +180,10 @@ var (
|
|||
privKey: hexToBytes("b3629de8ef6a275b4ffae41aa2bbbc2952eb92282ea6402435abbb010ecc1fb8"),
|
||||
privKeyWIF: "L3EQsGeEnyXmKaux54cG4DQeCSQDvGuvEuy3W2ss4geum7AtWaHw",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 1,
|
||||
Index: 3,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 1,
|
||||
Index: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -186,9 +196,10 @@ var (
|
|||
privKey: hexToBytes("ca747a7ef815ea0dbe68655272cecbfbd65f2a109019a9ed28e0d3dcaffe05c3"),
|
||||
privKeyWIF: "L41Frac75RPbTELKzw1EGC2qCkdveiVumpmsyX4daAvyyCMxit1W",
|
||||
derivationInfo: DerivationPath{
|
||||
Account: 0,
|
||||
Branch: 1,
|
||||
Index: 4,
|
||||
InternalAccount: 0,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 1,
|
||||
Index: 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
289
waddrmgr/db.go
289
waddrmgr/db.go
|
@ -6,6 +6,7 @@
|
|||
package waddrmgr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
|
@ -83,6 +84,12 @@ const (
|
|||
// database. This is an account that re-uses the key derivation schema
|
||||
// of BIP0044-like accounts.
|
||||
accountDefault accountType = 0 // not iota as they need to be stable
|
||||
|
||||
// accountWatchOnly is the account type used for storing watch-only
|
||||
// accounts within the database. This is an account that re-uses the key
|
||||
// derivation schema of BIP0044-like accounts and does not store private
|
||||
// keys.
|
||||
accountWatchOnly accountType = 1
|
||||
)
|
||||
|
||||
// dbAccountRow houses information stored about an account in the database.
|
||||
|
@ -102,6 +109,18 @@ type dbDefaultAccountRow struct {
|
|||
name string
|
||||
}
|
||||
|
||||
// dbWatchOnlyAccountRow houses additional information stored about a watch-only
|
||||
// account in the databse.
|
||||
type dbWatchOnlyAccountRow struct {
|
||||
dbAccountRow
|
||||
pubKeyEncrypted []byte
|
||||
masterKeyFingerprint uint32
|
||||
nextExternalIndex uint32
|
||||
nextInternalIndex uint32
|
||||
name string
|
||||
addrSchema *ScopeAddrSchema
|
||||
}
|
||||
|
||||
// dbAddressRow houses common information stored about an address in the
|
||||
// database.
|
||||
type dbAddressRow struct {
|
||||
|
@ -809,6 +828,159 @@ func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte,
|
|||
return rawData
|
||||
}
|
||||
|
||||
// deserializeWatchOnlyAccountRow deserializes the raw data from the passed
|
||||
// account row as a watch-only account.
|
||||
func deserializeWatchOnlyAccountRow(accountID []byte,
|
||||
row *dbAccountRow) (*dbWatchOnlyAccountRow, error) {
|
||||
|
||||
// The serialized BIP0044 watch-only account raw data format is:
|
||||
// <encpubkeylen><encpubkey><masterkeyfingerprint><nextextidx>
|
||||
// <nextintidx><namelen><name>
|
||||
//
|
||||
// 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key
|
||||
// fingerprint + 4 bytes next external index + 4 bytes next internal
|
||||
// index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes
|
||||
// addr schema (if exists)
|
||||
|
||||
// Given the above, the length of the entry must be at a minimum
|
||||
// the constant value sizes.
|
||||
if len(row.rawData) < 21 {
|
||||
str := fmt.Sprintf("malformed serialized watch-only account "+
|
||||
"for key %x", accountID)
|
||||
return nil, managerError(ErrDatabase, str, nil)
|
||||
}
|
||||
|
||||
retRow := dbWatchOnlyAccountRow{
|
||||
dbAccountRow: *row,
|
||||
}
|
||||
r := bytes.NewReader(row.rawData)
|
||||
|
||||
var pubLen uint32
|
||||
err := binary.Read(r, binary.LittleEndian, &pubLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
retRow.pubKeyEncrypted = make([]byte, pubLen)
|
||||
err = binary.Read(r, binary.LittleEndian, &retRow.pubKeyEncrypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &retRow.masterKeyFingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &retRow.nextExternalIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Read(r, binary.LittleEndian, &retRow.nextInternalIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nameLen uint32
|
||||
err = binary.Read(r, binary.LittleEndian, &nameLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := make([]byte, nameLen)
|
||||
err = binary.Read(r, binary.LittleEndian, &name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
retRow.name = string(name)
|
||||
|
||||
var addrSchemaExists bool
|
||||
err = binary.Read(r, binary.LittleEndian, &addrSchemaExists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if addrSchemaExists {
|
||||
var addrSchemaBytes [2]byte
|
||||
err = binary.Read(r, binary.LittleEndian, &addrSchemaBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
retRow.addrSchema = scopeSchemaFromBytes(addrSchemaBytes[:])
|
||||
}
|
||||
|
||||
return &retRow, nil
|
||||
}
|
||||
|
||||
// serializeWatchOnlyAccountRow returns the serialization of the raw data field
|
||||
// for a watch-only account.
|
||||
func serializeWatchOnlyAccountRow(encryptedPubKey []byte, masterKeyFingerprint,
|
||||
nextExternalIndex, nextInternalIndex uint32, name string,
|
||||
addrSchema *ScopeAddrSchema) ([]byte, error) {
|
||||
|
||||
// The serialized BIP0044 account raw data format is:
|
||||
// <encpubkeylen><encpubkey><masterkeyfingerprint><nextextidx>
|
||||
// <nextintidx><namelen><name>
|
||||
//
|
||||
// 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes master key
|
||||
// fingerprint + 4 bytes next external index + 4 bytes next internal
|
||||
// index + 4 bytes name len + name + 1 byte addr schema exists + 2 bytes
|
||||
// addr schema (if exists)
|
||||
pubLen := uint32(len(encryptedPubKey))
|
||||
nameLen := uint32(len(name))
|
||||
|
||||
addrSchemaExists := addrSchema != nil
|
||||
var addrSchemaBytes []byte
|
||||
if addrSchemaExists {
|
||||
addrSchemaBytes = scopeSchemaToBytes(addrSchema)
|
||||
}
|
||||
|
||||
bufLen := 21 + pubLen + nameLen + uint32(len(addrSchemaBytes))
|
||||
buf := bytes.NewBuffer(make([]byte, 0, bufLen))
|
||||
|
||||
err := binary.Write(buf, binary.LittleEndian, pubLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Write(buf, binary.LittleEndian, encryptedPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Write(buf, binary.LittleEndian, masterKeyFingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Write(buf, binary.LittleEndian, nextExternalIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Write(buf, binary.LittleEndian, nextInternalIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Write(buf, binary.LittleEndian, nameLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Write(buf, binary.LittleEndian, []byte(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Write(buf, binary.LittleEndian, addrSchemaExists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if addrSchemaExists {
|
||||
err = binary.Write(buf, binary.LittleEndian, addrSchemaBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// forEachKeyScope calls the given function for each known manager scope
|
||||
// within the set of scopes known by the root manager.
|
||||
func forEachKeyScope(ns walletdb.ReadBucket, fn func(KeyScope) error) error {
|
||||
|
@ -947,6 +1119,8 @@ func fetchAccountInfo(ns walletdb.ReadBucket, scope *KeyScope,
|
|||
switch row.acctType {
|
||||
case accountDefault:
|
||||
return deserializeDefaultAccountRow(accountID, row)
|
||||
case accountWatchOnly:
|
||||
return deserializeWatchOnlyAccountRow(accountID, row)
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("unsupported account type '%d'", row.acctType)
|
||||
|
@ -1087,8 +1261,9 @@ func putAccountRow(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
|||
return nil
|
||||
}
|
||||
|
||||
// putAccountInfo stores the provided account information to the database.
|
||||
func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
||||
// putDefaultAccountInfo stores the provided default account information to the
|
||||
// database.
|
||||
func putDefaultAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
||||
account uint32, encryptedPubKey, encryptedPrivKey []byte,
|
||||
nextExternalIndex, nextInternalIndex uint32, name string) error {
|
||||
|
||||
|
@ -1103,7 +1278,38 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
|||
acctType: accountDefault,
|
||||
rawData: rawData,
|
||||
}
|
||||
if err := putAccountRow(ns, scope, account, &acctRow); err != nil {
|
||||
return putAccountInfo(ns, scope, account, &acctRow, name)
|
||||
}
|
||||
|
||||
// putWatchOnlyAccountInfo stores the provided watch-only account information to
|
||||
// the database.
|
||||
func putWatchOnlyAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
||||
account uint32, encryptedPubKey []byte, masterKeyFingerprint,
|
||||
nextExternalIndex, nextInternalIndex uint32, name string,
|
||||
addrSchema *ScopeAddrSchema) error {
|
||||
|
||||
rawData, err := serializeWatchOnlyAccountRow(
|
||||
encryptedPubKey, masterKeyFingerprint, nextExternalIndex,
|
||||
nextInternalIndex, name, addrSchema,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(roasbeef): pass scope bucket directly??
|
||||
|
||||
acctRow := dbAccountRow{
|
||||
acctType: accountWatchOnly,
|
||||
rawData: rawData,
|
||||
}
|
||||
return putAccountInfo(ns, scope, account, &acctRow, name)
|
||||
}
|
||||
|
||||
// putAccountInfo stores the provided account information to the database.
|
||||
func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
||||
account uint32, acctRow *dbAccountRow, name string) error {
|
||||
|
||||
if err := putAccountRow(ns, scope, account, acctRow); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1113,11 +1319,7 @@ func putAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
|||
}
|
||||
|
||||
// Update account name index.
|
||||
if err := putAccountNameIndex(ns, scope, account, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return putAccountNameIndex(ns, scope, account, name)
|
||||
}
|
||||
|
||||
// putLastAccount stores the provided metadata - last account - to the
|
||||
|
@ -1479,32 +1681,64 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arow, err := deserializeDefaultAccountRow(accountID, row)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
switch row.acctType {
|
||||
case accountDefault:
|
||||
arow, err := deserializeDefaultAccountRow(accountID, row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Increment the appropriate next index depending on whether the
|
||||
// branch is internal or external.
|
||||
nextExternalIndex := arow.nextExternalIndex
|
||||
nextInternalIndex := arow.nextInternalIndex
|
||||
if branch == InternalBranch {
|
||||
nextInternalIndex = index + 1
|
||||
} else {
|
||||
nextExternalIndex = index + 1
|
||||
}
|
||||
|
||||
// Reserialize the account with the updated index and store it.
|
||||
row.rawData = serializeDefaultAccountRow(
|
||||
arow.pubKeyEncrypted, arow.privKeyEncrypted,
|
||||
nextExternalIndex, nextInternalIndex, arow.name,
|
||||
)
|
||||
|
||||
case accountWatchOnly:
|
||||
arow, err := deserializeWatchOnlyAccountRow(accountID, row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Increment the appropriate next index depending on whether the
|
||||
// branch is internal or external.
|
||||
nextExternalIndex := arow.nextExternalIndex
|
||||
nextInternalIndex := arow.nextInternalIndex
|
||||
if branch == InternalBranch {
|
||||
nextInternalIndex = index + 1
|
||||
} else {
|
||||
nextExternalIndex = index + 1
|
||||
}
|
||||
|
||||
// Reserialize the account with the updated index and store it.
|
||||
row.rawData, err = serializeWatchOnlyAccountRow(
|
||||
arow.pubKeyEncrypted, arow.masterKeyFingerprint,
|
||||
nextExternalIndex, nextInternalIndex, arow.name,
|
||||
arow.addrSchema,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the appropriate next index depending on whether the branch
|
||||
// is internal or external.
|
||||
nextExternalIndex := arow.nextExternalIndex
|
||||
nextInternalIndex := arow.nextInternalIndex
|
||||
if branch == InternalBranch {
|
||||
nextInternalIndex = index + 1
|
||||
} else {
|
||||
nextExternalIndex = index + 1
|
||||
}
|
||||
|
||||
// Reserialize the account with the updated index and store it.
|
||||
row.rawData = serializeDefaultAccountRow(
|
||||
arow.pubKeyEncrypted, arow.privKeyEncrypted, nextExternalIndex,
|
||||
nextInternalIndex, arow.name,
|
||||
)
|
||||
err = bucket.Put(accountID, serializeAccountRow(row))
|
||||
if err != nil {
|
||||
str := fmt.Sprintf("failed to update next index for "+
|
||||
"address %x, account %d", addressID, account)
|
||||
return managerError(ErrDatabase, str, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1741,6 +1975,9 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket) error {
|
|||
str := "failed to delete account private key"
|
||||
return managerError(ErrDatabase, str, err)
|
||||
}
|
||||
|
||||
// Watch-only accounts don't contain any private keys.
|
||||
case accountWatchOnly:
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -144,6 +144,8 @@ type addrKey string
|
|||
type accountInfo struct {
|
||||
acctName string
|
||||
|
||||
acctType accountType
|
||||
|
||||
// The account key is used to derive the branches which in turn derive
|
||||
// the internal and external addresses. The accountKeyPriv will be nil
|
||||
// when the address manager is locked.
|
||||
|
@ -160,16 +162,63 @@ type accountInfo struct {
|
|||
// intended for internal wallet use such as change addresses.
|
||||
nextInternalIndex uint32
|
||||
lastInternalAddr ManagedAddress
|
||||
|
||||
// addrSchema serves as a way for an account to override its
|
||||
// corresponding address schema with a custom one. For example, this
|
||||
// could be used to import accounts that use the traditional BIP-0049
|
||||
// derivation scheme into our KeyScopeBIP-0049Plus manager.
|
||||
addrSchema *ScopeAddrSchema
|
||||
|
||||
// masterKeyFingerprint represents the fingerprint of the root key
|
||||
// corresponding to the master public key (also known as the key with
|
||||
// derivation path m/). This may be required by some hardware wallets
|
||||
// for proper identification and signing.
|
||||
masterKeyFingerprint uint32
|
||||
}
|
||||
|
||||
// AccountProperties contains properties associated with each account, such as
|
||||
// the account name, number, and the nubmer of derived and imported keys.
|
||||
type AccountProperties struct {
|
||||
AccountNumber uint32
|
||||
AccountName string
|
||||
// AccountNumber is the internal number used to reference the account.
|
||||
AccountNumber uint32
|
||||
|
||||
// AccountName is the user-identifying name of the account.
|
||||
AccountName string
|
||||
|
||||
// ExternalKeyCount is the number of internal keys that have been
|
||||
// derived for the account.
|
||||
ExternalKeyCount uint32
|
||||
|
||||
// InternalKeyCount is the number of internal keys that have been
|
||||
// derived for the account.
|
||||
InternalKeyCount uint32
|
||||
|
||||
// ImportedKeyCount is the number of imported keys found within the
|
||||
// account.
|
||||
ImportedKeyCount uint32
|
||||
|
||||
// AccountPubKey is the account's public key that can be used to
|
||||
// derive any address relevant to said account.
|
||||
//
|
||||
// NOTE: This may be nil for imported accounts.
|
||||
AccountPubKey *hdkeychain.ExtendedKey
|
||||
|
||||
// MasterKeyFingerprint represents the fingerprint of the root key
|
||||
// corresponding to the master public key (also known as the key with
|
||||
// derivation path m/). This may be required by some hardware wallets
|
||||
// for proper identification and signing.
|
||||
MasterKeyFingerprint uint32
|
||||
|
||||
// KeyScope is the key scope the account belongs to.
|
||||
KeyScope KeyScope
|
||||
|
||||
// IsWatchOnly indicates whether the is set up as watch-only, i.e., it
|
||||
// doesn't contain any private key information.
|
||||
IsWatchOnly bool
|
||||
|
||||
// AddrSchema, if non-nil, specifies an address schema override for
|
||||
// address generation only applicable to the account.
|
||||
AddrSchema *ScopeAddrSchema
|
||||
}
|
||||
|
||||
// unlockDeriveInfo houses the information needed to derive a private key for a
|
||||
|
@ -1178,9 +1227,9 @@ 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(
|
||||
ns, info.managedAddr.Account(), info.branch,
|
||||
info.index, true,
|
||||
addressKey, _, err := manager.deriveKeyFromPath(
|
||||
ns, info.managedAddr.InternalAccount(),
|
||||
info.branch, info.index, true,
|
||||
)
|
||||
if err != nil {
|
||||
m.lock()
|
||||
|
@ -1372,13 +1421,17 @@ func deriveCoinTypeKey(masterNode *hdkeychain.ExtendedKey,
|
|||
// The branch is 0 for external addresses and 1 for internal addresses.
|
||||
|
||||
// Derive the purpose key as a child of the master node.
|
||||
purpose, err := masterNode.Child(scope.Purpose + hdkeychain.HardenedKeyStart)
|
||||
purpose, err := masterNode.DeriveNonStandard(
|
||||
scope.Purpose + hdkeychain.HardenedKeyStart,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Derive the coin type key as a child of the purpose key.
|
||||
coinTypeKey, err := purpose.Child(scope.Coin + hdkeychain.HardenedKeyStart)
|
||||
coinTypeKey, err := purpose.DeriveNonStandard(
|
||||
scope.Coin + hdkeychain.HardenedKeyStart,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1401,7 +1454,9 @@ func deriveAccountKey(coinTypeKey *hdkeychain.ExtendedKey,
|
|||
}
|
||||
|
||||
// Derive the account key as a child of the coin type key.
|
||||
return coinTypeKey.Child(account + hdkeychain.HardenedKeyStart)
|
||||
return coinTypeKey.DeriveNonStandard(
|
||||
account + hdkeychain.HardenedKeyStart,
|
||||
)
|
||||
}
|
||||
|
||||
// checkBranchKeys ensures deriving the extended keys for the internal and
|
||||
|
@ -1416,12 +1471,12 @@ func deriveAccountKey(coinTypeKey *hdkeychain.ExtendedKey,
|
|||
// The branch is 0 for external addresses and 1 for internal addresses.
|
||||
func checkBranchKeys(acctKey *hdkeychain.ExtendedKey) error {
|
||||
// Derive the external branch as the first child of the account key.
|
||||
if _, err := acctKey.Child(ExternalBranch); err != nil {
|
||||
if _, err := acctKey.DeriveNonStandard(ExternalBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Derive the external branch as the second child of the account key.
|
||||
_, err := acctKey.Child(InternalBranch)
|
||||
// Derive the internal branch as the second child of the account key.
|
||||
_, err := acctKey.DeriveNonStandard(InternalBranch)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1672,7 +1727,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
|
|||
}
|
||||
|
||||
// Save the information for the default account to the database.
|
||||
err = putAccountInfo(
|
||||
err = putDefaultAccountInfo(
|
||||
ns, &scope, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0,
|
||||
defaultAccountName,
|
||||
)
|
||||
|
@ -1680,7 +1735,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket,
|
|||
return err
|
||||
}
|
||||
|
||||
return putAccountInfo(
|
||||
return putDefaultAccountInfo(
|
||||
ns, &scope, ImportedAddrAccount, nil, nil, 0, 0,
|
||||
ImportedAddrAccountName,
|
||||
)
|
||||
|
@ -1783,10 +1838,10 @@ func Create(ns walletdb.ReadWriteBucket,
|
|||
|
||||
pubParams := masterKeyPub.Marshal()
|
||||
|
||||
var privParams []byte = nil
|
||||
var privParams []byte
|
||||
var masterKeyPriv *snacl.SecretKey
|
||||
var cryptoKeyPrivEnc []byte = nil
|
||||
var cryptoKeyScriptEnc []byte = nil
|
||||
var cryptoKeyPrivEnc []byte
|
||||
var cryptoKeyScriptEnc []byte
|
||||
if !isWatchingOnly {
|
||||
masterKeyPriv, err = newSecretKey(&privPassphrase, config)
|
||||
if err != nil {
|
||||
|
|
|
@ -71,15 +71,15 @@ func failingSecretKeyGen(passphrase *[]byte,
|
|||
// blocks have been inserted and therefore some of the transaction outputs are
|
||||
// spent.
|
||||
type testContext struct {
|
||||
t *testing.T
|
||||
caseName string
|
||||
db walletdb.DB
|
||||
rootManager *Manager
|
||||
manager *ScopedKeyManager
|
||||
account uint32
|
||||
create bool
|
||||
unlocked bool
|
||||
watchingOnly bool
|
||||
t *testing.T
|
||||
caseName string
|
||||
db walletdb.DB
|
||||
rootManager *Manager
|
||||
manager *ScopedKeyManager
|
||||
internalAccount uint32
|
||||
create bool
|
||||
unlocked bool
|
||||
watchingOnly bool
|
||||
}
|
||||
|
||||
// addrType is the type of address being tested
|
||||
|
@ -114,7 +114,7 @@ func testNamePrefix(tc *testContext) string {
|
|||
prefix = "Create "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("(%s) %s account #%d", tc.caseName, prefix, tc.account)
|
||||
return fmt.Sprintf("(%s) %s account #%d", tc.caseName, prefix, tc.internalAccount)
|
||||
}
|
||||
|
||||
// testManagedPubKeyAddress ensures the data returned by all exported functions
|
||||
|
@ -293,9 +293,9 @@ func testManagedScriptAddress(tc *testContext, prefix string,
|
|||
func testAddress(tc *testContext, prefix string, gotAddr ManagedAddress,
|
||||
wantAddr *expectedAddr) bool {
|
||||
|
||||
if gotAddr.Account() != tc.account {
|
||||
if gotAddr.InternalAccount() != tc.internalAccount {
|
||||
tc.t.Errorf("ManagedAddress.Account: unexpected account - got "+
|
||||
"%d, want %d", gotAddr.Account(), tc.account)
|
||||
"%d, want %d", gotAddr.InternalAccount(), tc.internalAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -360,7 +360,9 @@ func testExternalAddresses(tc *testContext) bool {
|
|||
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
var err error
|
||||
addrs, err = tc.manager.NextExternalAddresses(ns, tc.account, 5)
|
||||
addrs, err = tc.manager.NextExternalAddresses(
|
||||
ns, tc.internalAccount, 5,
|
||||
)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -395,7 +397,9 @@ func testExternalAddresses(tc *testContext) bool {
|
|||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||
var err error
|
||||
lastAddr, err = tc.manager.LastExternalAddress(ns, tc.account)
|
||||
lastAddr, err = tc.manager.LastExternalAddress(
|
||||
ns, tc.internalAccount,
|
||||
)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -512,7 +516,9 @@ func testInternalAddresses(tc *testContext) bool {
|
|||
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
var err error
|
||||
addrs, err = tc.manager.NextInternalAddresses(ns, tc.account, 5)
|
||||
addrs, err = tc.manager.NextInternalAddresses(
|
||||
ns, tc.internalAccount, 5,
|
||||
)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -547,7 +553,9 @@ func testInternalAddresses(tc *testContext) bool {
|
|||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||
var err error
|
||||
lastAddr, err = tc.manager.LastInternalAddress(ns, tc.account)
|
||||
lastAddr, err = tc.manager.LastInternalAddress(
|
||||
ns, tc.internalAccount,
|
||||
)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -776,7 +784,7 @@ func testImportPrivateKey(tc *testContext) bool {
|
|||
}
|
||||
|
||||
// Only import the private keys when in the create phase of testing.
|
||||
tc.account = ImportedAddrAccount
|
||||
tc.internalAccount = ImportedAddrAccount
|
||||
prefix := testNamePrefix(tc) + " testImportPrivateKey"
|
||||
if tc.create {
|
||||
for i, test := range tests {
|
||||
|
@ -949,7 +957,7 @@ func testImportScript(tc *testContext) bool {
|
|||
}
|
||||
|
||||
// Only import the scripts when in the create phase of testing.
|
||||
tc.account = ImportedAddrAccount
|
||||
tc.internalAccount = ImportedAddrAccount
|
||||
prefix := testNamePrefix(tc)
|
||||
if tc.create {
|
||||
for i, test := range tests {
|
||||
|
@ -1320,7 +1328,7 @@ func testNewAccount(tc *testContext) bool {
|
|||
tc.unlocked = true
|
||||
|
||||
testName := "acct-create"
|
||||
expectedAccount := tc.account + 1
|
||||
expectedAccount := tc.internalAccount + 1
|
||||
if !tc.create {
|
||||
// Create a new account in open mode
|
||||
testName = "acct-open"
|
||||
|
@ -1480,7 +1488,7 @@ func testRenameAccount(tc *testContext) bool {
|
|||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||
var err error
|
||||
acctName, err = tc.manager.AccountName(ns, tc.account)
|
||||
acctName, err = tc.manager.AccountName(ns, tc.internalAccount)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -1490,7 +1498,7 @@ func testRenameAccount(tc *testContext) bool {
|
|||
testName := acctName + "-renamed"
|
||||
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
return tc.manager.RenameAccount(ns, tc.account, testName)
|
||||
return tc.manager.RenameAccount(ns, tc.internalAccount, testName)
|
||||
})
|
||||
if err != nil {
|
||||
tc.t.Errorf("RenameAccount: unexpected error: %v", err)
|
||||
|
@ -1500,7 +1508,7 @@ func testRenameAccount(tc *testContext) bool {
|
|||
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||
var err error
|
||||
newName, err = tc.manager.AccountName(ns, tc.account)
|
||||
newName, err = tc.manager.AccountName(ns, tc.internalAccount)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -1516,7 +1524,7 @@ func testRenameAccount(tc *testContext) bool {
|
|||
// Test duplicate account name error
|
||||
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
return tc.manager.RenameAccount(ns, tc.account, testName)
|
||||
return tc.manager.RenameAccount(ns, tc.internalAccount, testName)
|
||||
})
|
||||
wantErrCode := ErrDuplicateAccount
|
||||
if !checkManagerError(tc.t, testName, err, wantErrCode) {
|
||||
|
@ -1587,7 +1595,7 @@ func testForEachAccountAddress(tc *testContext) bool {
|
|||
var addrs []ManagedAddress
|
||||
err := walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
|
||||
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||
return tc.manager.ForEachAccountAddress(ns, tc.account,
|
||||
return tc.manager.ForEachAccountAddress(ns, tc.internalAccount,
|
||||
func(maddr ManagedAddress) error {
|
||||
addrs = append(addrs, maddr)
|
||||
return nil
|
||||
|
@ -1632,14 +1640,14 @@ func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) {
|
|||
testChangePassphrase(tc)
|
||||
|
||||
// Reset default account
|
||||
tc.account = 0
|
||||
tc.internalAccount = 0
|
||||
testNewAccount(tc)
|
||||
testLookupAccount(tc)
|
||||
testForEachAccount(tc)
|
||||
testForEachAccountAddress(tc)
|
||||
|
||||
// Rename account 1 "acct-create"
|
||||
tc.account = 1
|
||||
tc.internalAccount = 1
|
||||
testRenameAccount(tc)
|
||||
} else {
|
||||
// Test API for created watch-only case.
|
||||
|
@ -1720,14 +1728,14 @@ func testConvertWatchingOnly(tc *testContext) bool {
|
|||
return false
|
||||
}
|
||||
testManagerAPI(&testContext{
|
||||
t: tc.t,
|
||||
caseName: tc.caseName,
|
||||
db: db,
|
||||
rootManager: mgr,
|
||||
manager: scopedMgr,
|
||||
account: 0,
|
||||
create: false,
|
||||
watchingOnly: true,
|
||||
t: tc.t,
|
||||
caseName: tc.caseName,
|
||||
db: db,
|
||||
rootManager: mgr,
|
||||
manager: scopedMgr,
|
||||
internalAccount: 0,
|
||||
create: false,
|
||||
watchingOnly: true,
|
||||
}, false)
|
||||
mgr.Close()
|
||||
|
||||
|
@ -1751,14 +1759,14 @@ func testConvertWatchingOnly(tc *testContext) bool {
|
|||
}
|
||||
|
||||
testManagerAPI(&testContext{
|
||||
t: tc.t,
|
||||
caseName: tc.caseName,
|
||||
db: db,
|
||||
rootManager: mgr,
|
||||
manager: scopedMgr,
|
||||
account: 0,
|
||||
create: false,
|
||||
watchingOnly: true,
|
||||
t: tc.t,
|
||||
caseName: tc.caseName,
|
||||
db: db,
|
||||
rootManager: mgr,
|
||||
manager: scopedMgr,
|
||||
internalAccount: 0,
|
||||
create: false,
|
||||
watchingOnly: true,
|
||||
}, false)
|
||||
|
||||
return true
|
||||
|
@ -1939,7 +1947,8 @@ func testManagerCase(t *testing.T, caseName string,
|
|||
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
_, err = scopedMgr.NewAccountWatchingOnly(
|
||||
ns, defaultAccountName, acctKeyPub)
|
||||
ns, defaultAccountName, acctKeyPub, 0, nil,
|
||||
)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -1951,14 +1960,14 @@ func testManagerCase(t *testing.T, caseName string,
|
|||
// Run all of the manager API tests in create mode and close the
|
||||
// manager after they've completed
|
||||
testManagerAPI(&testContext{
|
||||
t: t,
|
||||
caseName: caseName,
|
||||
db: db,
|
||||
manager: scopedMgr,
|
||||
rootManager: mgr,
|
||||
account: 0,
|
||||
create: true,
|
||||
watchingOnly: caseCreatedWatchingOnly,
|
||||
t: t,
|
||||
caseName: caseName,
|
||||
db: db,
|
||||
manager: scopedMgr,
|
||||
rootManager: mgr,
|
||||
internalAccount: 0,
|
||||
create: true,
|
||||
watchingOnly: caseCreatedWatchingOnly,
|
||||
}, caseCreatedWatchingOnly)
|
||||
mgr.Close()
|
||||
|
||||
|
@ -1981,14 +1990,14 @@ func testManagerCase(t *testing.T, caseName string,
|
|||
t.Fatalf("(%s) unable to fetch default scope: %v", caseName, err)
|
||||
}
|
||||
tc := &testContext{
|
||||
t: t,
|
||||
caseName: caseName,
|
||||
db: db,
|
||||
manager: scopedMgr,
|
||||
rootManager: mgr,
|
||||
account: 0,
|
||||
create: false,
|
||||
watchingOnly: caseCreatedWatchingOnly,
|
||||
t: t,
|
||||
caseName: caseName,
|
||||
db: db,
|
||||
manager: scopedMgr,
|
||||
rootManager: mgr,
|
||||
internalAccount: 0,
|
||||
create: false,
|
||||
watchingOnly: caseCreatedWatchingOnly,
|
||||
}
|
||||
testManagerAPI(tc, caseCreatedWatchingOnly)
|
||||
|
||||
|
@ -2640,7 +2649,9 @@ func TestNewRawAccountWatchingOnly(t *testing.T) {
|
|||
const accountNum = 1000
|
||||
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
return scopedMgr.NewRawAccountWatchingOnly(ns, accountNum, accountKey)
|
||||
return scopedMgr.NewRawAccountWatchingOnly(
|
||||
ns, accountNum, accountKey, 0, nil,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create new account: %v", err)
|
||||
|
@ -2705,7 +2716,9 @@ func TestNewRawAccountHybrid(t *testing.T) {
|
|||
const accountNum = 1000
|
||||
err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
return scopedMgr.NewRawAccountWatchingOnly(ns, accountNum, acctKeyPub)
|
||||
return scopedMgr.NewRawAccountWatchingOnly(
|
||||
ns, accountNum, acctKeyPub, 0, nil,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create new account: %v", err)
|
||||
|
@ -2743,9 +2756,10 @@ func testNewRawAccount(t *testing.T, mgr *Manager, db walletdb.DB,
|
|||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
|
||||
keyPath := DerivationPath{
|
||||
Account: accountNum,
|
||||
Branch: 0,
|
||||
Index: 0,
|
||||
InternalAccount: accountNum,
|
||||
Account: hdkeychain.HardenedKeyStart,
|
||||
Branch: 0,
|
||||
Index: 0,
|
||||
}
|
||||
accountTargetAddr, err = scopedMgr.DeriveFromKeyPath(
|
||||
ns, keyPath,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,8 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
)
|
||||
|
||||
// defaultDBTimeout specifies the timeout value when opening the wallet
|
||||
|
@ -51,3 +53,48 @@ func testWallet(t *testing.T) (*Wallet, func()) {
|
|||
|
||||
return w, cleanup
|
||||
}
|
||||
|
||||
// testWalletWatchingOnly creates a test watch only wallet and unlocks it.
|
||||
func testWalletWatchingOnly(t *testing.T) (*Wallet, func()) {
|
||||
// Set up a wallet.
|
||||
dir, err := ioutil.TempDir("", "test_wallet_watch_only")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create db dir: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Fatalf("could not cleanup test: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
pubPass := []byte("hello")
|
||||
loader := NewLoader(
|
||||
&chaincfg.TestNet3Params, dir, true, defaultDBTimeout, 250,
|
||||
)
|
||||
w, err := loader.CreateNewWatchingOnlyWallet(pubPass, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create wallet: %v", err)
|
||||
}
|
||||
chainClient := &mockChainClient{}
|
||||
w.chainClient = chainClient
|
||||
|
||||
err = walletdb.Update(w.Database(), func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
for scope, schema := range waddrmgr.ScopeAddrMap {
|
||||
_, err := w.Manager.NewScopedKeyManager(
|
||||
ns, scope, schema,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create default scopes: %v", err)
|
||||
}
|
||||
|
||||
return w, cleanup
|
||||
}
|
||||
|
|
365
wallet/import.go
Normal file
365
wallet/import.go
Normal file
|
@ -0,0 +1,365 @@
|
|||
package wallet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/btcsuite/btcwallet/walletdb"
|
||||
)
|
||||
|
||||
const (
|
||||
// accountPubKeyDepth is the maximum depth of an extended key for an
|
||||
// account public key.
|
||||
accountPubKeyDepth = 3
|
||||
|
||||
// pubKeyDepth is the depth of an extended key for a derived public key.
|
||||
pubKeyDepth = 5
|
||||
)
|
||||
|
||||
// keyScopeFromPubKey returns the corresponding wallet key scope for the given
|
||||
// extended public key. The address type can usually be inferred from the key's
|
||||
// version, but may be required for certain keys to map them into the proper
|
||||
// scope.
|
||||
func keyScopeFromPubKey(pubKey *hdkeychain.ExtendedKey,
|
||||
addrType *waddrmgr.AddressType) (waddrmgr.KeyScope,
|
||||
*waddrmgr.ScopeAddrSchema, error) {
|
||||
|
||||
switch waddrmgr.HDVersion(binary.BigEndian.Uint32(pubKey.Version())) {
|
||||
// For BIP-0044 keys, an address type must be specified as we intend to
|
||||
// not support importing BIP-0044 keys into the wallet using the legacy
|
||||
// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will
|
||||
// force the standard BIP-0049 derivation scheme (nested witness pubkeys
|
||||
// everywhere), while a witness address type will force the standard
|
||||
// BIP-0084 derivation scheme.
|
||||
case waddrmgr.HDVersionMainNetBIP0044, waddrmgr.HDVersionTestNetBIP0044:
|
||||
if addrType == nil {
|
||||
return waddrmgr.KeyScope{}, nil, errors.New("address " +
|
||||
"type must be specified for account public " +
|
||||
"key with legacy version")
|
||||
}
|
||||
|
||||
switch *addrType {
|
||||
case waddrmgr.NestedWitnessPubKey:
|
||||
return waddrmgr.KeyScopeBIP0049Plus,
|
||||
&waddrmgr.KeyScopeBIP0049AddrSchema, nil
|
||||
|
||||
case waddrmgr.WitnessPubKey:
|
||||
return waddrmgr.KeyScopeBIP0084, nil, nil
|
||||
|
||||
default:
|
||||
return waddrmgr.KeyScope{}, nil,
|
||||
fmt.Errorf("unsupported address type %v",
|
||||
*addrType)
|
||||
}
|
||||
|
||||
// For BIP-0049 keys, we'll need to make a distinction between the
|
||||
// traditional BIP-0049 address schema (nested witness pubkeys
|
||||
// everywhere) and our own BIP-0049Plus address schema (nested
|
||||
// externally, witness internally).
|
||||
case waddrmgr.HDVersionMainNetBIP0049, waddrmgr.HDVersionTestNetBIP0049:
|
||||
if addrType == nil {
|
||||
return waddrmgr.KeyScope{}, nil, errors.New("address " +
|
||||
"type must be specified for account public " +
|
||||
"key with BIP-0049 version")
|
||||
}
|
||||
|
||||
switch *addrType {
|
||||
case waddrmgr.NestedWitnessPubKey:
|
||||
return waddrmgr.KeyScopeBIP0049Plus,
|
||||
&waddrmgr.KeyScopeBIP0049AddrSchema, nil
|
||||
|
||||
case waddrmgr.WitnessPubKey:
|
||||
return waddrmgr.KeyScopeBIP0049Plus, nil, nil
|
||||
|
||||
default:
|
||||
return waddrmgr.KeyScope{}, nil,
|
||||
fmt.Errorf("unsupported address type %v",
|
||||
*addrType)
|
||||
}
|
||||
|
||||
case waddrmgr.HDVersionMainNetBIP0084, waddrmgr.HDVersionTestNetBIP0084:
|
||||
if addrType != nil && *addrType != waddrmgr.WitnessPubKey {
|
||||
return waddrmgr.KeyScope{}, nil,
|
||||
errors.New("address type mismatch")
|
||||
}
|
||||
return waddrmgr.KeyScopeBIP0084, nil, nil
|
||||
|
||||
default:
|
||||
return waddrmgr.KeyScope{}, nil, fmt.Errorf("unknown version %x",
|
||||
pubKey.Version())
|
||||
}
|
||||
}
|
||||
|
||||
// isPubKeyForNet determines if the given public key is for the current network
|
||||
// the wallet is operating under.
|
||||
func (w *Wallet) isPubKeyForNet(pubKey *hdkeychain.ExtendedKey) bool {
|
||||
version := waddrmgr.HDVersion(binary.BigEndian.Uint32(pubKey.Version()))
|
||||
switch w.chainParams.Net {
|
||||
case wire.MainNet:
|
||||
return version == waddrmgr.HDVersionMainNetBIP0044 ||
|
||||
version == waddrmgr.HDVersionMainNetBIP0049 ||
|
||||
version == waddrmgr.HDVersionMainNetBIP0084
|
||||
|
||||
case wire.TestNet, wire.TestNet3:
|
||||
return version == waddrmgr.HDVersionTestNetBIP0044 ||
|
||||
version == waddrmgr.HDVersionTestNetBIP0049 ||
|
||||
version == waddrmgr.HDVersionTestNetBIP0084
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// validateExtendedPubKey ensures a sane derived public key is provided.
|
||||
func (w *Wallet) validateExtendedPubKey(pubKey *hdkeychain.ExtendedKey,
|
||||
isAccountKey bool) error {
|
||||
|
||||
// Private keys are not allowed.
|
||||
if pubKey.IsPrivate() {
|
||||
return errors.New("private keys cannot be imported")
|
||||
}
|
||||
|
||||
// The public key must have a version corresponding to the current
|
||||
// chain.
|
||||
if !w.isPubKeyForNet(pubKey) {
|
||||
return fmt.Errorf("expected extended public key for current "+
|
||||
"network %v", w.chainParams.Name)
|
||||
}
|
||||
|
||||
// Verify the extended public key's depth and child index based on
|
||||
// whether it's an account key or not.
|
||||
if isAccountKey {
|
||||
if pubKey.Depth() != accountPubKeyDepth {
|
||||
return errors.New("invalid account key, must be of the " +
|
||||
"form m/purpose'/coin_type'/account'")
|
||||
}
|
||||
if pubKey.ChildIndex() < hdkeychain.HardenedKeyStart {
|
||||
return errors.New("invalid account key, must be hardened")
|
||||
}
|
||||
} else {
|
||||
if pubKey.Depth() != pubKeyDepth {
|
||||
return errors.New("invalid account key, must be of the " +
|
||||
"form m/purpose'/coin_type'/account'/change/" +
|
||||
"address_index")
|
||||
}
|
||||
if pubKey.ChildIndex() >= hdkeychain.HardenedKeyStart {
|
||||
return errors.New("invalid pulic key, must not be " +
|
||||
"hardened")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportAccount imports an account backed by an account extended public key.
|
||||
// The master key fingerprint denotes the fingerprint of the root key
|
||||
// corresponding to the account public key (also known as the key with
|
||||
// derivation path m/). This may be required by some hardware wallets for proper
|
||||
// identification and signing.
|
||||
//
|
||||
// The address type can usually be inferred from the key's version, but may be
|
||||
// required for certain keys to map them into the proper scope.
|
||||
//
|
||||
// For BIP-0044 keys, an address type must be specified as we intend to not
|
||||
// support importing BIP-0044 keys into the wallet using the legacy
|
||||
// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will force
|
||||
// the standard BIP-0049 derivation scheme, while a witness address type will
|
||||
// force the standard BIP-0084 derivation scheme.
|
||||
//
|
||||
// For BIP-0049 keys, an address type must also be specified to make a
|
||||
// distinction between the traditional BIP-0049 address schema (nested witness
|
||||
// pubkeys everywhere) and our own BIP-0049Plus address schema (nested
|
||||
// externally, witness internally).
|
||||
func (w *Wallet) ImportAccount(name string, accountPubKey *hdkeychain.ExtendedKey,
|
||||
masterKeyFingerprint uint32, addrType *waddrmgr.AddressType) (
|
||||
*waddrmgr.AccountProperties, error) {
|
||||
|
||||
// Ensure we have a valid account public key.
|
||||
if err := w.validateExtendedPubKey(accountPubKey, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine what key scope the account public key should belong to and
|
||||
// whether it should use a custom address schema.
|
||||
keyScope, addrSchema, err := keyScopeFromPubKey(accountPubKey, addrType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scopedMgr, err := w.Manager.FetchScopedKeyManager(keyScope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store the account as watch-only within the database.
|
||||
var accountProps *waddrmgr.AccountProperties
|
||||
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
account, err := scopedMgr.NewAccountWatchingOnly(
|
||||
ns, name, accountPubKey, masterKeyFingerprint,
|
||||
addrSchema,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accountProps, err = scopedMgr.AccountProperties(ns, account)
|
||||
return err
|
||||
})
|
||||
return accountProps, err
|
||||
}
|
||||
|
||||
// ImportPublicKey imports a single derived public key into the address manager.
|
||||
// The address type can usually be inferred from the key's version, but in the
|
||||
// case of legacy versions (xpub, tpub), an address type must be specified as we
|
||||
// intend to not support importing BIP-44 keys into the wallet using the legacy
|
||||
// pay-to-pubkey-hash (P2PKH) scheme.
|
||||
func (w *Wallet) ImportPublicKey(pubKey *btcec.PublicKey,
|
||||
addrType waddrmgr.AddressType) error {
|
||||
|
||||
// Determine what key scope the public key should belong to and import
|
||||
// it into the key scope's default imported account.
|
||||
var keyScope waddrmgr.KeyScope
|
||||
switch addrType {
|
||||
case waddrmgr.NestedWitnessPubKey:
|
||||
keyScope = waddrmgr.KeyScopeBIP0049Plus
|
||||
case waddrmgr.WitnessPubKey:
|
||||
keyScope = waddrmgr.KeyScopeBIP0084
|
||||
default:
|
||||
return fmt.Errorf("address type %v is not supported", addrType)
|
||||
}
|
||||
|
||||
scopedKeyManager, err := w.Manager.FetchScopedKeyManager(keyScope)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Perform rescan if requested.
|
||||
var addr waddrmgr.ManagedAddress
|
||||
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
addr, err = scopedKeyManager.ImportPublicKey(ns, pubKey, nil)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Imported address %v", addr.Address())
|
||||
|
||||
err = w.chainClient.NotifyReceived([]btcutil.Address{addr.Address()})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to subscribe for address "+
|
||||
"notifications: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportPrivateKey imports a private key to the wallet and writes the new
|
||||
// wallet to disk.
|
||||
//
|
||||
// NOTE: If a block stamp is not provided, then the wallet's birthday will be
|
||||
// set to the genesis block of the corresponding chain.
|
||||
func (w *Wallet) ImportPrivateKey(scope waddrmgr.KeyScope, wif *btcutil.WIF,
|
||||
bs *waddrmgr.BlockStamp, rescan bool) (string, error) {
|
||||
|
||||
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// The starting block for the key is the genesis block unless otherwise
|
||||
// specified.
|
||||
if bs == nil {
|
||||
bs = &waddrmgr.BlockStamp{
|
||||
Hash: *w.chainParams.GenesisHash,
|
||||
Height: 0,
|
||||
Timestamp: w.chainParams.GenesisBlock.Header.Timestamp,
|
||||
}
|
||||
} else if bs.Timestamp.IsZero() {
|
||||
// Only update the new birthday time from default value if we
|
||||
// actually have timestamp info in the header.
|
||||
header, err := w.chainClient.GetBlockHeader(&bs.Hash)
|
||||
if err == nil {
|
||||
bs.Timestamp = header.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to import private key into wallet.
|
||||
var addr btcutil.Address
|
||||
var props *waddrmgr.AccountProperties
|
||||
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
||||
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
maddr, err := manager.ImportPrivateKey(addrmgrNs, wif, bs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr = maddr.Address()
|
||||
props, err = manager.AccountProperties(
|
||||
addrmgrNs, waddrmgr.ImportedAddrAccount,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll only update our birthday with the new one if it is
|
||||
// before our current one. Otherwise, if we do, we can
|
||||
// potentially miss detecting relevant chain events that
|
||||
// occurred between them while rescanning.
|
||||
birthdayBlock, _, err := w.Manager.BirthdayBlock(addrmgrNs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bs.Height >= birthdayBlock.Height {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = w.Manager.SetBirthday(addrmgrNs, bs.Timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// To ensure this birthday block is correct, we'll mark it as
|
||||
// unverified to prompt a sanity check at the next restart to
|
||||
// ensure it is correct as it was provided by the caller.
|
||||
return w.Manager.SetBirthdayBlock(addrmgrNs, *bs, false)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Rescan blockchain for transactions with txout scripts paying to the
|
||||
// imported address.
|
||||
if rescan {
|
||||
job := &RescanJob{
|
||||
Addrs: []btcutil.Address{addr},
|
||||
OutPoints: nil,
|
||||
BlockStamp: *bs,
|
||||
}
|
||||
|
||||
// Submit rescan job and log when the import has completed.
|
||||
// Do not block on finishing the rescan. The rescan success
|
||||
// or failure is logged elsewhere, and the channel is not
|
||||
// required to be read, so discard the return value.
|
||||
_ = w.SubmitRescan(job)
|
||||
} else {
|
||||
err := w.chainClient.NotifyReceived([]btcutil.Address{addr})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to subscribe for address ntfns for "+
|
||||
"address %s: %s", addr.EncodeAddress(), err)
|
||||
}
|
||||
}
|
||||
|
||||
addrStr := addr.EncodeAddress()
|
||||
log.Infof("Imported payment address %s", addrStr)
|
||||
|
||||
w.NtfnServer.notifyAccountProperties(props)
|
||||
|
||||
// Return the payment address string of the imported private key.
|
||||
return addrStr, nil
|
||||
}
|
282
wallet/import_test.go
Normal file
282
wallet/import_test.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
package wallet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func hardenedKey(key uint32) uint32 {
|
||||
return key + hdkeychain.HardenedKeyStart
|
||||
}
|
||||
|
||||
func deriveAcctPubKey(t *testing.T, root *hdkeychain.ExtendedKey,
|
||||
scope waddrmgr.KeyScope, paths ...uint32) *hdkeychain.ExtendedKey {
|
||||
|
||||
path := []uint32{hardenedKey(scope.Purpose), hardenedKey(scope.Coin)}
|
||||
path = append(path, paths...)
|
||||
|
||||
var (
|
||||
currentKey = root
|
||||
err error
|
||||
)
|
||||
for _, pathPart := range path {
|
||||
currentKey, err = currentKey.Derive(pathPart)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// The Neuter() method checks the version and doesn't know any
|
||||
// non-standard methods. We need to convert them to standard, neuter,
|
||||
// then convert them back with the target extended public key version.
|
||||
pubVersionBytes := make([]byte, 4)
|
||||
copy(pubVersionBytes, chaincfg.TestNet3Params.HDPublicKeyID[:])
|
||||
switch {
|
||||
case strings.HasPrefix(root.String(), "uprv"):
|
||||
binary.BigEndian.PutUint32(pubVersionBytes, uint32(
|
||||
waddrmgr.HDVersionTestNetBIP0049,
|
||||
))
|
||||
|
||||
case strings.HasPrefix(root.String(), "vprv"):
|
||||
binary.BigEndian.PutUint32(pubVersionBytes, uint32(
|
||||
waddrmgr.HDVersionTestNetBIP0084,
|
||||
))
|
||||
}
|
||||
|
||||
currentKey, err = currentKey.CloneWithVersion(
|
||||
chaincfg.TestNet3Params.HDPrivateKeyID[:],
|
||||
)
|
||||
require.NoError(t, err)
|
||||
currentKey, err = currentKey.Neuter()
|
||||
require.NoError(t, err)
|
||||
currentKey, err = currentKey.CloneWithVersion(pubVersionBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
return currentKey
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
masterPriv string
|
||||
accountIndex uint32
|
||||
addrType waddrmgr.AddressType
|
||||
addrSchemaOverride *waddrmgr.ScopeAddrSchema
|
||||
expectedScope waddrmgr.KeyScope
|
||||
expectedAddr string
|
||||
expectedChangeAddr string
|
||||
}
|
||||
|
||||
var (
|
||||
testCases = []*testCase{{
|
||||
name: "bip44 with nested witness address type",
|
||||
masterPriv: "tprv8ZgxMBicQKsPeWwrFuNjEGTTDSY4mRLwd2KDJAPGa1AY" +
|
||||
"quw38bZqNMSuB3V1Va3hqJBo9Pt8Sx7kBQer5cNMrb8SYquoWPt9" +
|
||||
"Y3BZdhdtUcw",
|
||||
accountIndex: 0,
|
||||
addrType: waddrmgr.NestedWitnessPubKey,
|
||||
expectedScope: waddrmgr.KeyScopeBIP0049Plus,
|
||||
expectedAddr: "2N5YTxG9XtGXx1YyhZb7N2pwEjoZLLMHGKj",
|
||||
expectedChangeAddr: "2N7wpz5Gy2zEJTvq2MAuU6BCTEBLXNQ8dUw",
|
||||
}, {
|
||||
name: "bip44 with witness address type",
|
||||
masterPriv: "tprv8ZgxMBicQKsPeWwrFuNjEGTTDSY4mRLwd2KDJAPGa1AY" +
|
||||
"quw38bZqNMSuB3V1Va3hqJBo9Pt8Sx7kBQer5cNMrb8SYquoWPt9" +
|
||||
"Y3BZdhdtUcw",
|
||||
accountIndex: 777,
|
||||
addrType: waddrmgr.WitnessPubKey,
|
||||
expectedScope: waddrmgr.KeyScopeBIP0084,
|
||||
expectedAddr: "tb1qllxcutkzsukf8u8c8stkp464j0esu9xq7qju8x",
|
||||
expectedChangeAddr: "tb1qu6jmqglrthscptjqj3egx54wy8xqvzn5hslgw7",
|
||||
}, {
|
||||
name: "traditional bip49",
|
||||
masterPriv: "uprv8tXDerPXZ1QsVp8y6GAMSMYxPQgWi3LSY8qS5ZH9x1YRu" +
|
||||
"1kGPFjPzR73CFSbVUhdEwJbtsUgucUJ4hGQoJnNepp3RBcE6Jhdom" +
|
||||
"FD2KeY6G9",
|
||||
accountIndex: 9,
|
||||
addrType: waddrmgr.NestedWitnessPubKey,
|
||||
addrSchemaOverride: &waddrmgr.KeyScopeBIP0049AddrSchema,
|
||||
expectedScope: waddrmgr.KeyScopeBIP0049Plus,
|
||||
expectedAddr: "2NBCJ9WzGXZqpLpXGq3Hacybj3c4eHRcqgh",
|
||||
expectedChangeAddr: "2N3bankFu6F3ZNU41iVJQqyS9MXqp9dvn1M",
|
||||
}, {
|
||||
name: "bip49+",
|
||||
masterPriv: "uprv8tXDerPXZ1QsVp8y6GAMSMYxPQgWi3LSY8qS5ZH9x1YRu" +
|
||||
"1kGPFjPzR73CFSbVUhdEwJbtsUgucUJ4hGQoJnNepp3RBcE6Jhdom" +
|
||||
"FD2KeY6G9",
|
||||
accountIndex: 9,
|
||||
addrType: waddrmgr.WitnessPubKey,
|
||||
expectedScope: waddrmgr.KeyScopeBIP0049Plus,
|
||||
expectedAddr: "2NBCJ9WzGXZqpLpXGq3Hacybj3c4eHRcqgh",
|
||||
expectedChangeAddr: "tb1qeqn05w2hfq6axpdprhs4y7x65gxkkvfvyxqk4u",
|
||||
}, {
|
||||
name: "bip84",
|
||||
masterPriv: "vprv9DMUxX4ShgxMM7L5vcwyeSeTZNpxefKwTFMerxB3L1vJ" +
|
||||
"x7ZVdutxcUmBDTQBVPMYeaRQeM5FNGpqwysyX1CPT4VeHXJegDX8" +
|
||||
"5VJrQvaFaz3",
|
||||
accountIndex: 1,
|
||||
addrType: waddrmgr.WitnessPubKey,
|
||||
expectedScope: waddrmgr.KeyScopeBIP0084,
|
||||
expectedAddr: "tb1q5vepvcl0z8xj7kps4rsux722r4dvfwlhk6j532",
|
||||
expectedChangeAddr: "tb1qlwe2kgxcsa8x4huu79yff4rze0l5mwafg5c7xd",
|
||||
}}
|
||||
)
|
||||
|
||||
// TestImportAccount tests that extended public keys can successfully be
|
||||
// imported into both watch only and normal wallets.
|
||||
func TestImportAccount(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w, cleanup := testWallet(t)
|
||||
defer cleanup()
|
||||
|
||||
testImportAccount(t, w, tc, false, tc.name)
|
||||
})
|
||||
|
||||
name := tc.name + " watch-only"
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w, cleanup := testWalletWatchingOnly(t)
|
||||
defer cleanup()
|
||||
|
||||
testImportAccount(t, w, tc, true, name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
|
||||
name string) {
|
||||
|
||||
// First derive the master public key of the account we want to import.
|
||||
root, err := hdkeychain.NewKeyFromString(tc.masterPriv)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Derive the extended private and public key for our target account.
|
||||
acct1Pub := deriveAcctPubKey(
|
||||
t, root, tc.expectedScope, hardenedKey(tc.accountIndex),
|
||||
)
|
||||
|
||||
// We want to make sure we can import and handle multiple accounts, so
|
||||
// we create another one.
|
||||
acct2Pub := deriveAcctPubKey(
|
||||
t, root, tc.expectedScope, hardenedKey(tc.accountIndex+1),
|
||||
)
|
||||
|
||||
// And we also want to be able to import loose extended public keys
|
||||
// without needing to specify an explicit scope.
|
||||
acct3ExternalExtPub := deriveAcctPubKey(
|
||||
t, root, tc.expectedScope, hardenedKey(tc.accountIndex+2), 0, 0,
|
||||
)
|
||||
acct3ExternalPub, err := acct3ExternalExtPub.ECPubKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Import the extended public keys into new accounts.
|
||||
acct1, err := w.ImportAccount(
|
||||
name+"1", acct1Pub, root.ParentFingerprint(), &tc.addrType,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedScope, acct1.KeyScope)
|
||||
|
||||
acct2, err := w.ImportAccount(
|
||||
name+"2", acct2Pub, root.ParentFingerprint(), &tc.addrType,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedScope, acct2.KeyScope)
|
||||
|
||||
err = w.ImportPublicKey(acct3ExternalPub, tc.addrType)
|
||||
require.NoError(t, err)
|
||||
|
||||
// If the wallet is watch only, there is no default account and our
|
||||
// imported account will be index 0.
|
||||
firstAccountIndex := uint32(1)
|
||||
if watchOnly {
|
||||
firstAccountIndex = 0
|
||||
}
|
||||
|
||||
// We should have 3 additional accounts now.
|
||||
acctResult, err := w.Accounts(tc.expectedScope)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, acctResult.Accounts, int(firstAccountIndex*2)+2)
|
||||
|
||||
// Validate the state of the accounts.
|
||||
require.Equal(t, firstAccountIndex, acct1.AccountNumber)
|
||||
require.Equal(t, name+"1", acct1.AccountName)
|
||||
require.Equal(t, true, acct1.IsWatchOnly)
|
||||
require.Equal(t, root.ParentFingerprint(), acct1.MasterKeyFingerprint)
|
||||
require.NotNil(t, acct1.AccountPubKey)
|
||||
require.Equal(t, acct1Pub.String(), acct1.AccountPubKey.String())
|
||||
require.Equal(t, uint32(0), acct1.InternalKeyCount)
|
||||
require.Equal(t, uint32(0), acct1.ExternalKeyCount)
|
||||
require.Equal(t, uint32(0), acct1.ImportedKeyCount)
|
||||
|
||||
require.Equal(t, firstAccountIndex+1, acct2.AccountNumber)
|
||||
require.Equal(t, name+"2", acct2.AccountName)
|
||||
require.Equal(t, true, acct2.IsWatchOnly)
|
||||
require.Equal(t, root.ParentFingerprint(), acct2.MasterKeyFingerprint)
|
||||
require.NotNil(t, acct2.AccountPubKey)
|
||||
require.Equal(t, acct2Pub.String(), acct2.AccountPubKey.String())
|
||||
require.Equal(t, uint32(0), acct2.InternalKeyCount)
|
||||
require.Equal(t, uint32(0), acct2.ExternalKeyCount)
|
||||
require.Equal(t, uint32(0), acct2.ImportedKeyCount)
|
||||
|
||||
// Test address derivation.
|
||||
addr, err := w.NewAddress(acct1.AccountNumber, tc.expectedScope)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedAddr, addr.String())
|
||||
addr, err = w.NewChangeAddress(acct1.AccountNumber, tc.expectedScope)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedChangeAddr, addr.String())
|
||||
|
||||
// Make sure the key count was increased.
|
||||
acct1, err = w.AccountProperties(tc.expectedScope, acct1.AccountNumber)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1), acct1.InternalKeyCount)
|
||||
require.Equal(t, uint32(1), acct1.ExternalKeyCount)
|
||||
require.Equal(t, uint32(0), acct1.ImportedKeyCount)
|
||||
|
||||
// Make sure we can't get private keys for the imported accounts.
|
||||
_, err = w.DumpWIFPrivateKey(addr)
|
||||
require.True(t, waddrmgr.IsError(err, waddrmgr.ErrWatchingOnly))
|
||||
|
||||
// Get the address info for the single key we imported.
|
||||
switch tc.addrType {
|
||||
case waddrmgr.NestedWitnessPubKey:
|
||||
witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||
btcutil.Hash160(acct3ExternalPub.SerializeCompressed()),
|
||||
&chaincfg.TestNet3Params,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
witnessProg, err := txscript.PayToAddrScript(witnessAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr, err = btcutil.NewAddressScriptHash(
|
||||
witnessProg, &chaincfg.TestNet3Params,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
case waddrmgr.WitnessPubKey:
|
||||
addr, err = btcutil.NewAddressWitnessPubKeyHash(
|
||||
btcutil.Hash160(acct3ExternalPub.SerializeCompressed()),
|
||||
&chaincfg.TestNet3Params,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
default:
|
||||
t.Fatalf("unhandled address type %v", tc.addrType)
|
||||
}
|
||||
|
||||
addrManaged, err := w.AddressInfo(addr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, addrManaged.Imported())
|
||||
}
|
|
@ -90,7 +90,7 @@ func lookupOutputChain(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetail
|
|||
if err != nil {
|
||||
log.Errorf("Cannot fetch account for wallet output: %v", err)
|
||||
} else {
|
||||
account = ma.Account()
|
||||
account = ma.InternalAccount()
|
||||
internal = ma.Internal()
|
||||
}
|
||||
return
|
||||
|
|
118
wallet/wallet.go
118
wallet/wallet.go
|
@ -946,18 +946,20 @@ func expandScopeHorizons(ns walletdb.ReadWriteBucket,
|
|||
// externalKeyPath returns the relative external derivation path /0/0/index.
|
||||
func externalKeyPath(index uint32) waddrmgr.DerivationPath {
|
||||
return waddrmgr.DerivationPath{
|
||||
Account: waddrmgr.DefaultAccountNum,
|
||||
Branch: waddrmgr.ExternalBranch,
|
||||
Index: index,
|
||||
InternalAccount: waddrmgr.DefaultAccountNum,
|
||||
Account: waddrmgr.DefaultAccountNum,
|
||||
Branch: waddrmgr.ExternalBranch,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
|
||||
// internalKeyPath returns the relative internal derivation path /0/1/index.
|
||||
func internalKeyPath(index uint32) waddrmgr.DerivationPath {
|
||||
return waddrmgr.DerivationPath{
|
||||
Account: waddrmgr.DefaultAccountNum,
|
||||
Branch: waddrmgr.InternalBranch,
|
||||
Index: index,
|
||||
InternalAccount: waddrmgr.DefaultAccountNum,
|
||||
Account: waddrmgr.DefaultAccountNum,
|
||||
Branch: waddrmgr.InternalBranch,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2733,110 +2735,6 @@ func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
|
|||
return wif.String(), nil
|
||||
}
|
||||
|
||||
// ImportPrivateKey imports a private key to the wallet and writes the new
|
||||
// wallet to disk.
|
||||
//
|
||||
// NOTE: If a block stamp is not provided, then the wallet's birthday will be
|
||||
// set to the genesis block of the corresponding chain.
|
||||
func (w *Wallet) ImportPrivateKey(scope waddrmgr.KeyScope, wif *btcutil.WIF,
|
||||
bs *waddrmgr.BlockStamp, rescan bool) (string, error) {
|
||||
|
||||
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// The starting block for the key is the genesis block unless otherwise
|
||||
// specified.
|
||||
if bs == nil {
|
||||
bs = &waddrmgr.BlockStamp{
|
||||
Hash: *w.chainParams.GenesisHash,
|
||||
Height: 0,
|
||||
Timestamp: w.chainParams.GenesisBlock.Header.Timestamp,
|
||||
}
|
||||
} else if bs.Timestamp.IsZero() {
|
||||
// Only update the new birthday time from default value if we
|
||||
// actually have timestamp info in the header.
|
||||
header, err := w.chainClient.GetBlockHeader(&bs.Hash)
|
||||
if err == nil {
|
||||
bs.Timestamp = header.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to import private key into wallet.
|
||||
var addr btcutil.Address
|
||||
var props *waddrmgr.AccountProperties
|
||||
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
||||
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||
maddr, err := manager.ImportPrivateKey(addrmgrNs, wif, bs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr = maddr.Address()
|
||||
props, err = manager.AccountProperties(
|
||||
addrmgrNs, waddrmgr.ImportedAddrAccount,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll only update our birthday with the new one if it is
|
||||
// before our current one. Otherwise, if we do, we can
|
||||
// potentially miss detecting relevant chain events that
|
||||
// occurred between them while rescanning.
|
||||
birthdayBlock, _, err := w.Manager.BirthdayBlock(addrmgrNs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bs.Height >= birthdayBlock.Height {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = w.Manager.SetBirthday(addrmgrNs, bs.Timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// To ensure this birthday block is correct, we'll mark it as
|
||||
// unverified to prompt a sanity check at the next restart to
|
||||
// ensure it is correct as it was provided by the caller.
|
||||
return w.Manager.SetBirthdayBlock(addrmgrNs, *bs, false)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Rescan blockchain for transactions with txout scripts paying to the
|
||||
// imported address.
|
||||
if rescan {
|
||||
job := &RescanJob{
|
||||
Addrs: []btcutil.Address{addr},
|
||||
OutPoints: nil,
|
||||
BlockStamp: *bs,
|
||||
}
|
||||
|
||||
// Submit rescan job and log when the import has completed.
|
||||
// Do not block on finishing the rescan. The rescan success
|
||||
// or failure is logged elsewhere, and the channel is not
|
||||
// required to be read, so discard the return value.
|
||||
_ = w.SubmitRescan(job)
|
||||
} else {
|
||||
err := w.chainClient.NotifyReceived([]btcutil.Address{addr})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to subscribe for address ntfns for "+
|
||||
"address %s: %s", addr.EncodeAddress(), err)
|
||||
}
|
||||
}
|
||||
|
||||
addrStr := addr.EncodeAddress()
|
||||
log.Infof("Imported payment address %s", addrStr)
|
||||
|
||||
w.NtfnServer.notifyAccountProperties(props)
|
||||
|
||||
// Return the payment address string of the imported private key.
|
||||
return addrStr, nil
|
||||
}
|
||||
|
||||
// LockedOutpoint returns whether an outpoint has been marked as locked and
|
||||
// should not be used as an input for created transactions.
|
||||
func (w *Wallet) LockedOutpoint(op wire.OutPoint) bool {
|
||||
|
|
|
@ -2284,11 +2284,11 @@ func TestTxLabel(t *testing.T) {
|
|||
defer teardown()
|
||||
|
||||
// txid is the transaction hash we will use to write and get labels for.
|
||||
txid := TstRecvTx.Hash()
|
||||
txid := &chainhash.Hash{1}
|
||||
|
||||
// txidNotFound is distinct from txid, and will not have a label written
|
||||
// to disk.
|
||||
txidNotFound := TstSpendingTx.Hash()
|
||||
txidNotFound := &chainhash.Hash{2}
|
||||
|
||||
// getBucket gets the top level bucket, and fails the test if it is
|
||||
// not found.
|
||||
|
|
Loading…
Reference in a new issue