bip46 1.0.0__tar.gz

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.
@@ -0,0 +1,27 @@
1
+ name: mypy
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ jobs:
8
+ check:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.12"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - name: Set up Python ${{ matrix.python-version }}
16
+ uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v6
21
+ with:
22
+ enable-cache: true
23
+ python-version: ${{ matrix.python-version }}
24
+ - name: Install dependencies
25
+ run: uv sync --locked --all-extras --dev
26
+ - name: Run mypy
27
+ run: make mypy
@@ -0,0 +1,27 @@
1
+ name: release
2
+ on:
3
+ push:
4
+ tags:
5
+ - "v[0-9]+.[0-9]+.[0-9]+"
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - name: Set up Python 3.12
12
+ uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.12"
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v6
17
+ - name: Build the project
18
+ run: uv build
19
+ - name: Publish to pypi
20
+ env:
21
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
22
+ run: uv publish
23
+ - name: Create github release
24
+ env:
25
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26
+ tag: ${{ github.ref_name }}
27
+ run: gh release create "$tag" --generate-notes
@@ -0,0 +1,12 @@
1
+ name: Ruff
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ jobs:
8
+ ruff:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/ruff-action@v3
@@ -0,0 +1,27 @@
1
+ name: tests
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.12"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - name: Set up Python ${{ matrix.python-version }}
16
+ uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v6
21
+ with:
22
+ enable-cache: true
23
+ python-version: ${{ matrix.python-version }}
24
+ - name: Install dependencies
25
+ run: uv sync --locked --all-extras --dev
26
+ - name: Test with pytest
27
+ run: make test
bip46-1.0.0/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ __pycache__
2
+ .env
@@ -0,0 +1 @@
1
+ 3.12
bip46-1.0.0/LICENSE ADDED
@@ -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.
bip46-1.0.0/Makefile ADDED
@@ -0,0 +1,19 @@
1
+ all: ruff mypy test
2
+ format: ruff
3
+ lint: ruff mypy
4
+ check: checkruff mypy test
5
+
6
+ install:
7
+ uv sync
8
+
9
+ ruff:
10
+ uv run ruff check . --fix
11
+
12
+ checkruff:
13
+ uv run ruff check .
14
+
15
+ mypy:
16
+ uv run mypy .
17
+
18
+ test:
19
+ uv run pytest tests
bip46-1.0.0/PKG-INFO ADDED
@@ -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
+ ```
bip46-1.0.0/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # BIP46 - Address Scheme for Timelocked Fidelity Bonds
2
+
3
+ Python implementation of BIP46: Address Scheme for Timelocked Fidelity Bonds.
4
+
5
+ ## Example
6
+
7
+ ### Create a redeem script from a mnemonic
8
+
9
+ ```python
10
+ from datetime import UTC, datetime
11
+
12
+ from bip46 import (
13
+ create_redeemscript,
14
+ hdkey_derive,
15
+ hdkey_from_mnemonic,
16
+ hdkey_to_pubkey,
17
+ lockdate_to_derivation_path,
18
+ redeemscript_address,
19
+ redeemscript_pubkey,
20
+ )
21
+
22
+ network = "mainnet"
23
+ lock_date = datetime(2042, 6, 1, tzinfo=UTC)
24
+ lock_path = lockdate_to_derivation_path(lock_date, network=network)
25
+
26
+ hdkey = hdkey_from_mnemonic(
27
+ "abandon abandon abandon abandon abandon abandon"
28
+ " abandon abandon abandon abandon abandon about",
29
+ network=network,
30
+ )
31
+ redeem_key = hdkey_derive(hdkey, lock_path)
32
+ redeem_pub_key = hdkey_to_pubkey(redeem_key)
33
+ redeem_script = create_redeemscript(lock_date, redeem_pub_key)
34
+ script_pubkey = redeemscript_pubkey(redeem_script)
35
+ script_address = redeemscript_address(script_pubkey, network=network)
36
+ ```
37
+
38
+ ### Create addresses from an xpub
39
+
40
+ BIP46 paths include hardened account components: `m/84'/coin_type'/0'/2/index`.
41
+ A public key cannot derive those hardened components, so pass an account-level xpub
42
+ for `m/84'/coin_type'/0'`. The library will derive the public BIP46 suffix
43
+ `2/index` from that account xpub.
44
+
45
+ ```python
46
+ from datetime import UTC, datetime
47
+
48
+ from bip46 import (
49
+ create_redeemscript,
50
+ hdkey_derive,
51
+ hdkey_from_xpub,
52
+ hdkey_to_pubkey,
53
+ lockdate_to_derivation_path,
54
+ redeemscript_address,
55
+ redeemscript_pubkey,
56
+ )
57
+
58
+ network = "mainnet"
59
+ lock_date = datetime(2042, 6, 1, tzinfo=UTC)
60
+ lock_path = lockdate_to_derivation_path(lock_date, network=network)
61
+
62
+ hdkey = hdkey_from_xpub("xpub...")
63
+ redeem_key = hdkey_derive(hdkey, lock_path)
64
+ redeem_pub_key = hdkey_to_pubkey(redeem_key)
65
+ redeem_script = create_redeemscript(lock_date, redeem_pub_key)
66
+ script_pubkey = redeemscript_pubkey(redeem_script)
67
+ script_address = redeemscript_address(script_pubkey, network=network)
68
+ ```
69
+
70
+ The CLI also accepts an account xpub:
71
+
72
+ ```sh
73
+ XPUB="xpub..." uv run bip46 create-timelock 2042 6 mainnet
74
+ ```
@@ -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
+ ]
@@ -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()
@@ -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()
@@ -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
+ }
@@ -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}"
@@ -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