bip46 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bip46/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ from embit.script import address_to_scriptpubkey
2
+ from embit.transaction import Transaction, TransactionInput, TransactionOutput
3
+
4
+ from .certificate import (
5
+ create_certificate_message,
6
+ recover_from_signature_and_message,
7
+ sign_certificate_message,
8
+ )
9
+ from .derivation import (
10
+ index_to_lockdate,
11
+ lockdate_to_derivation_path,
12
+ lockdate_to_index,
13
+ lockindex_to_derivation_path,
14
+ )
15
+ from .exceptions import (
16
+ Bip46Bech32Error,
17
+ Bip46IndexError,
18
+ Bip46PathError,
19
+ Bip46RecoverPubkeyError,
20
+ Bip46TimeError,
21
+ )
22
+ from .hdkey import (
23
+ hdkey_derive,
24
+ hdkey_from_mnemonic,
25
+ hdkey_from_seed,
26
+ hdkey_from_xpub,
27
+ hdkey_scan,
28
+ hdkey_scan_all,
29
+ hdkey_to_pubkey,
30
+ )
31
+ from .script import create_redeemscript, redeemscript_address, redeemscript_pubkey
32
+
33
+
34
+ # WIP
35
+ def create_redeem_transaction(
36
+ prevtxid: str,
37
+ vout: int,
38
+ redeemscript: bytes,
39
+ address: str,
40
+ fee_sats_vbyte: int = 10,
41
+ # network: str = DEFAULT_NETWORK,
42
+ ) -> str:
43
+ """Create a redeem transaction for a BIP46 timelock"""
44
+ tx_size = 100
45
+ amount = 100000 - fee_sats_vbyte * tx_size
46
+ receive_address = address_to_scriptpubkey(address)
47
+ txout = TransactionOutput(amount, receive_address)
48
+ txin = TransactionInput(prevtxid, vout, redeemscript)
49
+ tx = Transaction(2, [txin], [txout])
50
+ return tx.serialize().hex()
51
+
52
+
53
+ __all__ = [
54
+ "Bip46Bech32Error",
55
+ "Bip46IndexError",
56
+ "Bip46PathError",
57
+ "Bip46RecoverPubkeyError",
58
+ "Bip46TimeError",
59
+ "create_certificate_message",
60
+ "create_redeemscript",
61
+ "hdkey_derive",
62
+ "hdkey_from_mnemonic",
63
+ "hdkey_from_seed",
64
+ "hdkey_from_xpub",
65
+ "hdkey_scan",
66
+ "hdkey_scan_all",
67
+ "hdkey_to_pubkey",
68
+ "index_to_lockdate",
69
+ "lockdate_to_derivation_path",
70
+ "lockdate_to_index",
71
+ "lockindex_to_derivation_path",
72
+ "recover_from_signature_and_message",
73
+ "redeemscript_address",
74
+ "redeemscript_pubkey",
75
+ "sign_certificate_message",
76
+ ]
bip46/certificate.py ADDED
@@ -0,0 +1,69 @@
1
+ from base64 import b64decode
2
+ from hashlib import sha256
3
+
4
+ from secp256k1 import PrivateKey, PublicKey
5
+
6
+ from .exceptions import Bip46RecoverPubkeyError
7
+
8
+
9
+ def create_certificate_message(
10
+ pubkey: str,
11
+ message: str = "fidelity-bond-cert",
12
+ expiry: int = 375
13
+ ) -> str:
14
+ """Create a certificate message for a BIP46 timelock"""
15
+ return f"{message}|{pubkey}|{expiry}"
16
+
17
+
18
+ def prepare_certificate_message(message: str) -> bytes:
19
+ """ message hash for signing """
20
+ msg = message.encode()
21
+ prefix = b"\x18Bitcoin Signed Message:\n"
22
+ data = prefix + bytes([len(msg)]) + msg
23
+ return sha256(sha256(data).digest()).digest()
24
+
25
+
26
+ def sign_certificate_message(private_key: bytes, message: str) -> bytes:
27
+ """Sign a certificate message for a BIP46 timelock"""
28
+ message_hash = prepare_certificate_message(message)
29
+ key = PrivateKey(private_key)
30
+ raw_sig = key.ecdsa_sign_recoverable(message_hash, raw=True)
31
+ sig, recid = key.ecdsa_recoverable_serialize(raw_sig)
32
+ # 31 for p2pkh_address
33
+ return bytes([recid + 31]) + sig
34
+
35
+
36
+ def convert_recoverable_message_signature(sig_base64: str) -> tuple[bytes, int, bool]:
37
+ """ Convert a recoverable signature to a normal signature """
38
+ sig = b64decode(sig_base64)
39
+ if len(sig) != 65:
40
+ raise Bip46RecoverPubkeyError("Invalid signature length")
41
+ header = int(sig[0])
42
+ if header < 27 or header > 42:
43
+ raise Bip46RecoverPubkeyError(f"Header out of range: {header}")
44
+ r = sig[1:33]
45
+ s = sig[33:]
46
+ compressed = False
47
+ if(header >= 39): # this is a bech32 signature
48
+ header -= 12
49
+ compressed = True
50
+ # this is a segwit p2sh signature
51
+ elif header >= 35:
52
+ header -= 8
53
+ compressed = True
54
+ # this is a compressed key signature
55
+ elif header >= 31:
56
+ compressed = True
57
+ header -= 4
58
+ rec_id = header - 27
59
+ return r + s, rec_id, compressed
60
+
61
+
62
+ def recover_from_signature_and_message(sig_base64: str, message: str) -> bytes:
63
+ """Recover a pubkey from signature and message"""
64
+ message_hash = prepare_certificate_message(message)
65
+ sig, recid, _ = convert_recoverable_message_signature(sig_base64)
66
+ empty = PublicKey()
67
+ sig = empty.ecdsa_recoverable_deserialize(sig, recid)
68
+ pubkey = empty.ecdsa_recover(message_hash, sig, raw=True)
69
+ return PublicKey(pubkey).serialize()
bip46/cli.py ADDED
@@ -0,0 +1,135 @@
1
+ """ bip46 CLI """
2
+
3
+
4
+ import os
5
+ import sys
6
+ from datetime import UTC, datetime
7
+
8
+ import click
9
+ from embit.bip32 import HDKey
10
+
11
+ from bip46 import (
12
+ create_redeemscript,
13
+ hdkey_derive,
14
+ hdkey_from_mnemonic,
15
+ hdkey_from_seed,
16
+ hdkey_from_xpub,
17
+ hdkey_scan,
18
+ hdkey_scan_all,
19
+ hdkey_to_pubkey,
20
+ index_to_lockdate,
21
+ lockdate_to_derivation_path,
22
+ redeemscript_address,
23
+ redeemscript_pubkey,
24
+ )
25
+ from bip46.consts import DEFAULT_NETWORK
26
+
27
+ # disable tracebacks on exceptions
28
+ sys.tracebacklimit = 0
29
+
30
+ def _check_hdkey(network: str) -> HDKey:
31
+ """check if the environment is set"""
32
+ if "XPUB" in os.environ:
33
+ return hdkey_from_xpub(os.environ["XPUB"])
34
+ if "SEED" in os.environ:
35
+ return hdkey_from_seed(os.environ["SEED"].encode(), network)
36
+ if "MNEMONIC" in os.environ:
37
+ return hdkey_from_mnemonic(os.environ["MNEMONIC"], network)
38
+
39
+ click.echo("please set `XPUB`, `SEED` or `MNEMONIC` environment variable")
40
+ sys.exit(1)
41
+
42
+
43
+ @click.group()
44
+ def command_group():
45
+ """
46
+ Python CLI for bip46 timelocks\n
47
+ set `SEED` or `MNEMONIC` environment variable to set the seed
48
+ """
49
+
50
+
51
+ @click.command()
52
+ @click.argument("network", type=str, default=DEFAULT_NETWORK)
53
+ def scan_all(network: str):
54
+ """
55
+ scan for all timelocks
56
+ """
57
+ key = _check_hdkey(network)
58
+ bonds = hdkey_scan_all(key, network)
59
+ print(f"Found {len(bonds)} timelocked bonds:")
60
+ for bond in bonds:
61
+ print(bond.index, bond.txid, bond.address)
62
+
63
+
64
+ @click.command()
65
+ @click.argument("index", type=int)
66
+ @click.argument("network", type=str, default=DEFAULT_NETWORK)
67
+ def scan(index: int, network: str):
68
+ """
69
+ scan for a timelock at a specific index
70
+ """
71
+ key = _check_hdkey(network)
72
+ bonds = hdkey_scan(index, key, network)
73
+ print(f"Found {len(bonds)} timelocked bonds")
74
+ for bond in bonds:
75
+ print(bond.index, bond.txid, bond.address)
76
+
77
+
78
+ @click.command()
79
+ @click.argument("year", type=int)
80
+ @click.argument("month", type=int)
81
+ @click.argument("network", type=str, default=DEFAULT_NETWORK)
82
+ def create_timelock(year: int, month: int, network: str):
83
+ """
84
+ create a timelock address
85
+ """
86
+ key = _check_hdkey(network)
87
+ lock_date = datetime(year, month, 1, tzinfo=UTC)
88
+ lock_path = lockdate_to_derivation_path(lock_date, network)
89
+ redeem_child = hdkey_derive(key, lock_path)
90
+ redeem_pub_key = hdkey_to_pubkey(redeem_child)
91
+ redeem_script = create_redeemscript(lock_date, redeem_pub_key)
92
+ script_pubkey = redeemscript_pubkey(redeem_script)
93
+ script_address = redeemscript_address(script_pubkey, network=network)
94
+
95
+ print(f"network: {network}")
96
+ print(f"lock_date: {lock_date.isoformat()}")
97
+ print(f"lock_path: {lock_path}")
98
+ print(f"script address: {script_address}")
99
+
100
+
101
+ @click.command()
102
+ @click.argument("year", type=int)
103
+ @click.argument("month", type=int)
104
+ @click.argument("network", type=str, default=DEFAULT_NETWORK)
105
+ def get_derivation_path(year: int, month: int, network: str):
106
+ """
107
+ create a timelock address
108
+ """
109
+ lock_date = datetime(year, month, 1, tzinfo=UTC)
110
+ lock_path = lockdate_to_derivation_path(lock_date, network)
111
+ print(lock_path)
112
+
113
+
114
+ @click.command()
115
+ @click.argument("index", type=int)
116
+ def get_lockdate(index: int):
117
+ """
118
+ get the lock date from an index
119
+ """
120
+ lock_path = index_to_lockdate(index)
121
+ print(lock_path.isoformat())
122
+
123
+
124
+ def main():
125
+ """main function"""
126
+ command_group.add_command(create_timelock)
127
+ command_group.add_command(scan)
128
+ command_group.add_command(scan_all)
129
+ command_group.add_command(get_derivation_path)
130
+ command_group.add_command(get_lockdate)
131
+ command_group()
132
+
133
+
134
+ if __name__ == "__main__":
135
+ main()
bip46/consts.py ADDED
@@ -0,0 +1,25 @@
1
+ ELECTRS_SERVER = "https://testnet-electrs.b1tco1n.org"
2
+ MAX_INDEX = 959
3
+ DEFAULT_NETWORK = "testnet"
4
+ NETWORKS = {
5
+ "mainnet": {
6
+ "bech32": "bc",
7
+ "xprv": b"\x04\x88\xad\xe4",
8
+ "bip32": 0, # coin type for bip32 derivation
9
+ },
10
+ "testnet": {
11
+ "bech32": "tb",
12
+ "xprv": b"\x04\x35\x83\x94",
13
+ "bip32": 1,
14
+ },
15
+ "regtest": {
16
+ "bech32": "bcrt",
17
+ "xprv": b"\x04\x35\x83\x94",
18
+ "bip32": 1,
19
+ },
20
+ "signet": {
21
+ "bech32": "tb",
22
+ "xprv": b"\x04\x35\x83\x94",
23
+ "bip32": 1,
24
+ },
25
+ }
bip46/derivation.py ADDED
@@ -0,0 +1,37 @@
1
+ from datetime import UTC, datetime
2
+
3
+ from .consts import DEFAULT_NETWORK, NETWORKS
4
+ from .exceptions import Bip46IndexError, Bip46TimeError
5
+
6
+
7
+ def index_to_lockdate(index: int) -> datetime:
8
+ """Convert a BIP46 timelock index to a datetime"""
9
+ if index < 0 or index > 959:
10
+ raise Bip46IndexError("index must be between 0 and 959")
11
+ year = 2020 + index // 12
12
+ month = 1 + index % 12
13
+ day = 1
14
+ return datetime(year, month, day, tzinfo=UTC)
15
+
16
+
17
+ def lockdate_to_index(lock_date: datetime) -> int:
18
+ """Convert a datetime to a BIP46 timelock index"""
19
+ if lock_date.year < 2020:
20
+ raise Bip46TimeError("lock_date must be after 2020")
21
+ if lock_date.year > 2099:
22
+ raise Bip46TimeError("lock_date must be before 2100")
23
+ return (lock_date.year - 2020) * 12 + lock_date.month - 1
24
+
25
+
26
+ def lockdate_to_derivation_path(
27
+ lock_date: datetime, network: str = DEFAULT_NETWORK
28
+ ) -> str:
29
+ """Derive the path for a BIP46 timelock from a date"""
30
+ i = lockdate_to_index(lock_date)
31
+ return lockindex_to_derivation_path(i, network)
32
+
33
+
34
+ def lockindex_to_derivation_path(index: int, network: str = DEFAULT_NETWORK) -> str:
35
+ """Derive the path for a BIP46 timelock from an index"""
36
+ bip32 = NETWORKS[network]["bip32"]
37
+ return f"m/84'/{bip32}'/0'/2/{index}"
bip46/electrs.py ADDED
@@ -0,0 +1,38 @@
1
+
2
+ import httpx
3
+
4
+ from .consts import ELECTRS_SERVER
5
+
6
+
7
+ def request(url: str) -> dict:
8
+ with httpx.Client() as client:
9
+ resp = client.get(url)
10
+ resp.raise_for_status()
11
+ return resp.json()
12
+
13
+
14
+ def get_tx(txid: str, electrs_server: str = ELECTRS_SERVER) -> dict:
15
+ """Get a transaction from a block explorer"""
16
+ url = f"{electrs_server}/tx/{txid}"
17
+ return request(url)
18
+
19
+
20
+ def get_txs_from_address(address: str, electrs_server: str = ELECTRS_SERVER) -> dict:
21
+ """Get a transaction from a block explorer"""
22
+ url = f"{electrs_server}/address/{address}/txs"
23
+ return request(url)
24
+
25
+
26
+ def get_vout_from_tx(tx: dict, address: str) -> tuple[str, int, int] | None:
27
+ for i, out in enumerate(tx["vout"]):
28
+ if out["scriptpubkey_address"] == address:
29
+ return tx["txid"], i, out["value"]
30
+ return None
31
+
32
+
33
+ def get_vout_from_txs(txs: dict, address: str) -> tuple[str, int, int] | None:
34
+ for tx in txs:
35
+ vout = get_vout_from_tx(tx, address)
36
+ if vout:
37
+ return vout
38
+ return None
bip46/exceptions.py ADDED
@@ -0,0 +1,21 @@
1
+ """ Custom exceptions for the bip46 module """
2
+
3
+
4
+ class Bip46TimeError(Exception):
5
+ """Raised when a locktime is not in the valid range for BIP46 timelocks"""
6
+
7
+
8
+ class Bip46IndexError(Exception):
9
+ """Raised when a locktime index is not in the valid range for BIP46 timelocks"""
10
+
11
+
12
+ class Bip46PathError(Exception):
13
+ """Raised when the derivation path is invalid"""
14
+
15
+
16
+ class Bip46Bech32Error(Exception):
17
+ """Raised when the bech32 encoding fails"""
18
+
19
+
20
+ class Bip46RecoverPubkeyError(Exception):
21
+ """Raised when we could not recover the pubkey from signature and message"""
bip46/hdkey.py ADDED
@@ -0,0 +1,106 @@
1
+ from dataclasses import dataclass
2
+
3
+ from embit.bip32 import HDKey
4
+ from embit.bip39 import mnemonic_to_seed
5
+
6
+ from .consts import DEFAULT_NETWORK, MAX_INDEX, NETWORKS
7
+ from .derivation import index_to_lockdate, lockindex_to_derivation_path
8
+ from .electrs import get_txs_from_address
9
+ from .exceptions import Bip46PathError
10
+ from .script import create_redeemscript, redeemscript_address, redeemscript_pubkey
11
+
12
+
13
+ @dataclass
14
+ class Bond:
15
+ index: int
16
+ address: str
17
+ txid: str
18
+ hdkey: HDKey
19
+
20
+
21
+ def hdkey_scan(index: int, hdkey: HDKey, network: str = DEFAULT_NETWORK) -> list[Bond]:
22
+ """
23
+ Scan a path for all possible keys timelocks
24
+ """
25
+ bonds = []
26
+ path = lockindex_to_derivation_path(index, network)
27
+ hdkey_i = hdkey.derive(path)
28
+ pubkey = hdkey_to_pubkey(hdkey_i)
29
+ lock_date = index_to_lockdate(index)
30
+ redeemscript = create_redeemscript(lock_date, pubkey)
31
+ address = redeemscript_address(redeemscript_pubkey(redeemscript), network)
32
+ print(f"({index}/{MAX_INDEX}) Scanning timelocks for address {address}")
33
+ txs = get_txs_from_address(address)
34
+ if len(txs) > 0:
35
+ print(f"Found {len(txs)} timelocks for index {index} !!!")
36
+ for tx in txs:
37
+ bonds.append(Bond(index, address, tx.get("txid"), hdkey_i))
38
+ return bonds
39
+
40
+
41
+ def hdkey_scan_all(hdkey: HDKey, network: str = DEFAULT_NETWORK) -> list[Bond]:
42
+ """
43
+ Scan a path for all possible keys timelocks
44
+ """
45
+ bonds = []
46
+ for i in range(MAX_INDEX + 1):
47
+ bonds.extend(hdkey_scan(i, hdkey, network))
48
+ return bonds
49
+
50
+
51
+ def hdkey_from_seed(seed: bytes, network: str = DEFAULT_NETWORK) -> HDKey:
52
+ """Create a HDKey from a seed"""
53
+ version = NETWORKS[network]["xprv"]
54
+ master = HDKey.from_seed(seed, version=version)
55
+ return master
56
+
57
+
58
+ def hdkey_from_mnemonic(mnemonic: str, network: str = DEFAULT_NETWORK) -> HDKey:
59
+ """Create a HDKey from a mnemonic"""
60
+ seed = mnemonic_to_seed(mnemonic)
61
+ return hdkey_from_seed(seed, network)
62
+
63
+
64
+ def hdkey_from_xpub(xpub: str) -> HDKey:
65
+ """Create a public HDKey from an account-level xpub."""
66
+ hdkey = HDKey.from_base58(xpub)
67
+ if hdkey.is_private:
68
+ raise Bip46PathError("Expected a public extended key")
69
+ return hdkey
70
+
71
+
72
+ def hdkey_derive(hdkey: HDKey, path: str) -> HDKey:
73
+ """
74
+ Derive a child key from a hdkey
75
+ Path should be in the form of m/x/y/z where x' means hardened
76
+ first child path for mainnet is m/84'/0'/0'/2/0
77
+ """
78
+ if not path.startswith("m"):
79
+ raise Bip46PathError(f"Invalid Path, should start with `m`: {path}")
80
+ if not path.endswith(f"/2/{path.split('/')[-1]}"):
81
+ raise Bip46PathError(f"Invalid Path, should end with `/2/x`: {path}")
82
+ if not hdkey.is_private:
83
+ path = _account_xpub_path(hdkey, path)
84
+ return hdkey.derive(path)
85
+
86
+
87
+ def hdkey_to_pubkey(hdkey: HDKey) -> bytes:
88
+ """Get the pubkey from an HDKey"""
89
+ return hdkey.get_public_key().sec()
90
+
91
+
92
+ def _account_xpub_path(hdkey: HDKey, path: str) -> str:
93
+ if hdkey.depth != 3:
94
+ raise Bip46PathError(
95
+ "Public derivation requires an account-level xpub for m/84'/coin'/0'"
96
+ )
97
+
98
+ parts = path.split("/")
99
+ if len(parts) == 6 and all(part.endswith("'") for part in parts[1:4]):
100
+ return "m/" + "/".join(parts[4:])
101
+ if len(parts) == 3:
102
+ return path
103
+ raise Bip46PathError(
104
+ "Public derivation requires an account-level xpub for m/84'/coin'/0' "
105
+ "and a BIP46 path ending in /2/index"
106
+ )
bip46/py.typed ADDED
File without changes
bip46/script.py ADDED
@@ -0,0 +1,44 @@
1
+ from datetime import datetime
2
+ from hashlib import sha256
3
+
4
+ import bech32
5
+
6
+ from .consts import DEFAULT_NETWORK, NETWORKS
7
+ from .exceptions import Bip46Bech32Error
8
+
9
+
10
+ def lockdate_to_little_endian(locktime: datetime) -> bytes:
11
+ """Convert a lockdate to little-endian bytes for use in a script"""
12
+ # max signed int for 4bytes
13
+ max_int = 2**31 - 1
14
+ ts = int(locktime.timestamp())
15
+ size = 4 if ts <= max_int else 5
16
+ return ts.to_bytes(size, "little")
17
+
18
+
19
+ def create_redeemscript(lock_date: datetime, pubkey: bytes) -> bytes:
20
+ """Create a redeemscript pubkey for a BIP46 timelock"""
21
+ locktime_bytes = lockdate_to_little_endian(lock_date)
22
+ return (
23
+ bytes([len(locktime_bytes)]) # 1 byte len of locktime
24
+ + locktime_bytes # 4 or 5 bytes of locktime
25
+ + bytes([177, 117]) # OP_CLTV OP_DROP
26
+ + bytes([len(pubkey)]) # 1 byte len of pubkey
27
+ + pubkey # pubkey
28
+ + bytes([172]) # OP_CHECKSIG
29
+ )
30
+
31
+
32
+ def redeemscript_pubkey(redeemscript: bytes) -> bytes:
33
+ """Create a redeemscript pubkey for a BIP46 timelock"""
34
+ hashed = sha256(redeemscript).digest()
35
+ return bytes([0, len(hashed)]) + hashed
36
+
37
+
38
+ def redeemscript_address(script_pubkey: bytes, network: str = DEFAULT_NETWORK) -> str:
39
+ """Create a p2wpkh_address redeemscript address for a BIP46 timelock"""
40
+ hrp = NETWORKS[network]["bech32"]
41
+ address = bech32.encode(str(hrp), 0, script_pubkey[2:])
42
+ if not address:
43
+ raise Bip46Bech32Error("Could not encode address")
44
+ return address
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: bip46
3
+ Version: 1.0.0
4
+ Summary: A Python implementation of BIP46: Address Scheme for Timelocked Fidelity Bonds
5
+ Author-email: dni <dni@lnbits.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: bech32<2.0.0,>=1.2.0
10
+ Requires-Dist: click<9.0.0,>=8.1.7
11
+ Requires-Dist: embit<0.9.0,>=0.8.0
12
+ Requires-Dist: httpx<1.0.0,>=0.27.2
13
+ Requires-Dist: secp256k1<0.15.0,>=0.14.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # BIP46 - Address Scheme for Timelocked Fidelity Bonds
17
+
18
+ Python implementation of BIP46: Address Scheme for Timelocked Fidelity Bonds.
19
+
20
+ ## Example
21
+
22
+ ### Create a redeem script from a mnemonic
23
+
24
+ ```python
25
+ from datetime import UTC, datetime
26
+
27
+ from bip46 import (
28
+ create_redeemscript,
29
+ hdkey_derive,
30
+ hdkey_from_mnemonic,
31
+ hdkey_to_pubkey,
32
+ lockdate_to_derivation_path,
33
+ redeemscript_address,
34
+ redeemscript_pubkey,
35
+ )
36
+
37
+ network = "mainnet"
38
+ lock_date = datetime(2042, 6, 1, tzinfo=UTC)
39
+ lock_path = lockdate_to_derivation_path(lock_date, network=network)
40
+
41
+ hdkey = hdkey_from_mnemonic(
42
+ "abandon abandon abandon abandon abandon abandon"
43
+ " abandon abandon abandon abandon abandon about",
44
+ network=network,
45
+ )
46
+ redeem_key = hdkey_derive(hdkey, lock_path)
47
+ redeem_pub_key = hdkey_to_pubkey(redeem_key)
48
+ redeem_script = create_redeemscript(lock_date, redeem_pub_key)
49
+ script_pubkey = redeemscript_pubkey(redeem_script)
50
+ script_address = redeemscript_address(script_pubkey, network=network)
51
+ ```
52
+
53
+ ### Create addresses from an xpub
54
+
55
+ BIP46 paths include hardened account components: `m/84'/coin_type'/0'/2/index`.
56
+ A public key cannot derive those hardened components, so pass an account-level xpub
57
+ for `m/84'/coin_type'/0'`. The library will derive the public BIP46 suffix
58
+ `2/index` from that account xpub.
59
+
60
+ ```python
61
+ from datetime import UTC, datetime
62
+
63
+ from bip46 import (
64
+ create_redeemscript,
65
+ hdkey_derive,
66
+ hdkey_from_xpub,
67
+ hdkey_to_pubkey,
68
+ lockdate_to_derivation_path,
69
+ redeemscript_address,
70
+ redeemscript_pubkey,
71
+ )
72
+
73
+ network = "mainnet"
74
+ lock_date = datetime(2042, 6, 1, tzinfo=UTC)
75
+ lock_path = lockdate_to_derivation_path(lock_date, network=network)
76
+
77
+ hdkey = hdkey_from_xpub("xpub...")
78
+ redeem_key = hdkey_derive(hdkey, lock_path)
79
+ redeem_pub_key = hdkey_to_pubkey(redeem_key)
80
+ redeem_script = create_redeemscript(lock_date, redeem_pub_key)
81
+ script_pubkey = redeemscript_pubkey(redeem_script)
82
+ script_address = redeemscript_address(script_pubkey, network=network)
83
+ ```
84
+
85
+ The CLI also accepts an account xpub:
86
+
87
+ ```sh
88
+ XPUB="xpub..." uv run bip46 create-timelock 2042 6 mainnet
89
+ ```
@@ -0,0 +1,15 @@
1
+ bip46/__init__.py,sha256=QmOukqjhY3Wd1pw6YMjwcQ0rnrw1QD93obIguWJBWhE,1953
2
+ bip46/certificate.py,sha256=hK3ULNQKvyy79dvbd8N6wbZ1hySbGyNgX5WrJRYVQRc,2338
3
+ bip46/cli.py,sha256=h8OSBdSn71_C3Qhr5EUTLa3mnogE2fjWhw48djwBj4Y,3682
4
+ bip46/consts.py,sha256=9NyEhKSkKmZiNFZrrZCkTznL8btQN24dRiFhR5E2ud0,569
5
+ bip46/derivation.py,sha256=kFbu4zbZ0InSya3sNuJnoaxwc-Eibfgu1vHZ6dMpEr0,1290
6
+ bip46/electrs.py,sha256=3LJcE_UXL43kK74E25zrERm_iuuAw2n72dgrhrYBAyA,1043
7
+ bip46/exceptions.py,sha256=XchCU6VLnsUk-glgmAxtUzZa9NgMetXsGAopcrcULTc,580
8
+ bip46/hdkey.py,sha256=2U74EhFoNjXsKxHnZwcwe4CA7SeAnr-zjwx1QfB8i7Q,3466
9
+ bip46/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ bip46/script.py,sha256=a7cfNL59guLe8wPp_uyQAb-12_do3avOLsinou1aoug,1514
11
+ bip46-1.0.0.dist-info/METADATA,sha256=SPqaBnMgF2uoLOQw2l_DnOYkJoCjybEju9sdJT1qQTY,2573
12
+ bip46-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
13
+ bip46-1.0.0.dist-info/entry_points.txt,sha256=erLAErWuxedSmof6WoPRgby4KJuFotSBW_8Frln9v_Y,41
14
+ bip46-1.0.0.dist-info/licenses/LICENSE,sha256=5nXjp5shAeUfsQid_GPb2_rUzhDxjW4G96kn0RL8C94,1064
15
+ bip46-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bip46 = bip46.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 dni ⚡
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.