Attempt to generate watch-only wallets fails
ravi-ojha opened this issue · 8 comments
Theoretically, it should be possible to generate watch-only addresses from xpublic_key.
However, the code errors when attempting something like this
bip44_hdwallet: BIP44HDWallet = BIP44HDWallet(cryptocurrency=BitcoinMainnet)
bip44_hdwallet.from_xpublic_key(xpublic_key=XPUB_KEY)
for address_index in range(10):
bip44_derivation = BIP44Derivation(
cryptocurrency=BitcoinMainnet, account=0, change=False, address=address_index
)
bip44_hdwallet.from_path(path=bip44_derivation)
print(f"({address_index}) {bip44_hdwallet.path()} {bip44_hdwallet.address()}")
bip44_hdwallet.clean_derivation()
The error is thrown by _derive_key_by_index
function.
437 i_str = struct.pack(">L", index)
438 if index & BIP32KEY_HARDEN:
--> 439 data = b"\0" + self._key.to_string() + i_str
440 else:
441 data = unhexlify(self.public_key()) + i_str
AttributeError: 'NoneType' object has no attribute 'to_string'
Of course, in watch-only wallets, private key would be null, and it's trying to access _key
which is empty as expected. Hence the failure.
@ravi-ojha The hardened derivation path does not work for xpublic key, it only works for xprivate key. But, In BIP44HDWallet
the default derivation has a hardened m/44'
index. So, Instead of this use your own derivation path and use HDWallet
class, Sir.
For example but first upgrade it into v1.3.1
hdwallet: HDWallet = HDWallet(cryptocurrency=BitcoinMainnet)
# hdwallet.from_xpublic_key(xpublic_key=XPUB_KEY)
hdwallet.from_root_xpublic_key(xpublic_key=XPUB_KEY, strict=False)
for address_index in range(10):
# hdwallet.from_path(path=f"m/44/{BitcoinMainnet.COIN_TYPE.INDEX}/0/0/{address_index}")
hdwallet.from_index(44, hardened=False)
hdwallet.from_index(BitcoinMainnet.COIN_TYPE.INDEX, hardened=False)
hdwallet.from_index(0, hardened=False)
hdwallet.from_index(0, hardened=False)
hdwallet.from_index(address_index, hardened=False)
print(f"({address_index}) {hdwallet.path()} {hdwallet.p2pkh_address()}") # hdwallet.p2pkh_address() the same with bip44_hdwallet.address()
hdwallet.clean_derivation()
Maybe I should've added more context and explanation in the first comment.
Let's take an example: say m/49'/0'/0'
(i.e. hardened upto account
going by m / purpose' / coin_type' / account' / change / address_index
)
Using an xpub for path m/49'/0'/0'
, theoretically, we can generate all the addresses and change addresses for one account. That is to say m/49'/0'/0'/i/j
where 0 <= i,j <= 2^32-1
That's because i
and j
are not hardened. It's not advisable to move away from the standard m/49'/0'/0'
hardened up to the account in my use case.
I ended up doing this using electrum library and with the following snippet.
from electrum import bitcoin
from electrum.bip32 import BIP32Node
def get_p2wpkh_p2sh_btc_address(xpub: str, account_idx: int = 0, change: int = 0):
bip32_node = BIP32Node.from_xkey(xpub)
path = "{}/{}".format(change, account_idx)
public_key_hex = bip32_node.subkey_at_public_derivation(
path
).eckey.get_public_key_hex()
return bitcoin.pubkey_to_address("p2wpkh-p2sh", public_key_hex)
Just to avoid any confusion, the xpub
here is "Account Extended Public Key" from https://iancoleman.io/bip39/
We can make the function generic but that's all I needed for one-time use.
Use this example derivation m/49/0/0/i/j
instead of this m/49'/0'/0'/i/j
path. If any derivation path has this '
symbol (hardened), it doesn't work for xpublic key derivation, Sir.
Hey @meherett Are you sure you read my above comment? I added an example script that generates the addresses (and change addresses) using electrum wallet library with just xpublic key.
This is how watch-only wallets work.
Yes @ravi-ojha. It's not working also here https://iancoleman.io/bip39/
from electrum import bitcoin
from electrum.bip32 import BIP32Node
could you use it for other currencies? ETH, LTC, etc
Thanks"