bitwalkit 0.1.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.
Files changed (35) hide show
  1. bitwalkit-0.1.0/COPYING +21 -0
  2. bitwalkit-0.1.0/MANIFEST.in +3 -0
  3. bitwalkit-0.1.0/PKG-INFO +116 -0
  4. bitwalkit-0.1.0/README.md +99 -0
  5. bitwalkit-0.1.0/pyproject.toml +30 -0
  6. bitwalkit-0.1.0/setup.cfg +4 -0
  7. bitwalkit-0.1.0/src/bitwalkit/__init__.py +74 -0
  8. bitwalkit-0.1.0/src/bitwalkit/_electrum.py +136 -0
  9. bitwalkit-0.1.0/src/bitwalkit/_secp.py +126 -0
  10. bitwalkit-0.1.0/src/bitwalkit/address.py +190 -0
  11. bitwalkit-0.1.0/src/bitwalkit/bip32.py +204 -0
  12. bitwalkit-0.1.0/src/bitwalkit/chain.py +150 -0
  13. bitwalkit-0.1.0/src/bitwalkit/descriptor.py +41 -0
  14. bitwalkit-0.1.0/src/bitwalkit/encoding.py +176 -0
  15. bitwalkit-0.1.0/src/bitwalkit/errors.py +39 -0
  16. bitwalkit-0.1.0/src/bitwalkit/hashing.py +157 -0
  17. bitwalkit-0.1.0/src/bitwalkit/hd.py +91 -0
  18. bitwalkit-0.1.0/src/bitwalkit/rpc.py +107 -0
  19. bitwalkit-0.1.0/src/bitwalkit.egg-info/PKG-INFO +116 -0
  20. bitwalkit-0.1.0/src/bitwalkit.egg-info/SOURCES.txt +33 -0
  21. bitwalkit-0.1.0/src/bitwalkit.egg-info/dependency_links.txt +1 -0
  22. bitwalkit-0.1.0/src/bitwalkit.egg-info/requires.txt +5 -0
  23. bitwalkit-0.1.0/src/bitwalkit.egg-info/top_level.txt +1 -0
  24. bitwalkit-0.1.0/tests/test_chain.py +92 -0
  25. bitwalkit-0.1.0/tests/test_derivation.py +115 -0
  26. bitwalkit-0.1.0/tests/test_descriptor.py +28 -0
  27. bitwalkit-0.1.0/tests/test_electrum.py +60 -0
  28. bitwalkit-0.1.0/tests/test_encoding.py +71 -0
  29. bitwalkit-0.1.0/tests/test_rpc.py +89 -0
  30. bitwalkit-0.1.0/tests/test_rpc_fixtures.py +98 -0
  31. bitwalkit-0.1.0/tests/test_rpc_http.py +80 -0
  32. bitwalkit-0.1.0/tests/test_secp.py +68 -0
  33. bitwalkit-0.1.0/tests/test_upstream_vectors.py +59 -0
  34. bitwalkit-0.1.0/tests/vectors/bip341_wallet_vectors.json +452 -0
  35. bitwalkit-0.1.0/tests/vectors/generated.json +399 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Elmeri Niemelä
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include COPYING
2
+ include README.md
3
+ recursive-include tests/vectors *.json
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: bitwalkit
3
+ Version: 0.1.0
4
+ Summary: Standalone watch-only Bitcoin wallet derivation and monitoring toolkit
5
+ Author: Elmeri Niemelä
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/elmeriniemela/bitwalkit
8
+ Keywords: bitcoin,rpc,bip32,electrum,wallet,watch-only
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+ License-File: COPYING
12
+ Provides-Extra: dev
13
+ Requires-Dist: build>=1.3; extra == "dev"
14
+ Requires-Dist: pyright>=1.1.400; extra == "dev"
15
+ Requires-Dist: pytest>=8; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # bitwalkit — Watch-only Bitcoin wallet toolkit
19
+
20
+ `bitwalkit` is a standalone, dependency-free Python toolkit for deriving and
21
+ monitoring Bitcoin watch-only wallets. It handles public wallet data only: the
22
+ library does not accept private extended keys, hold secrets, or sign
23
+ transactions.
24
+
25
+ Its public API covers three tasks:
26
+
27
+ 1. Derive single-signature and multisignature addresses from extended public
28
+ keys with `Account` and `MultisigAccount`.
29
+ 2. Query address balances, UTXOs, and transaction history through an Electrum
30
+ server with `ChainQuery`.
31
+ 3. Call a Bitcoin Core node over JSON-RPC with `NodeRPC`.
32
+
33
+ The runtime uses only the Python standard library. A small, public-key-only
34
+ secp256k1 implementation validates compressed public keys, derives
35
+ non-hardened BIP32 child public keys, and applies the BIP341/BIP86 Taproot
36
+ output-key tweak. It is not suitable for private-key handling or transaction
37
+ signing. There are no packages to install at runtime.
38
+
39
+ ## Supported wallets
40
+
41
+ - Single-key P2PKH (BIP44), P2SH-P2WPKH (BIP49), P2WPKH (BIP84), and P2TR
42
+ (BIP86)
43
+ - P2WSH and P2SH-P2WSH multisig (BIP48), with optional BIP67 key sorting
44
+ - Mainnet, testnet, and regtest addresses
45
+ - BIP32 and SLIP-0132 public prefixes: xpub/tpub, ypub/upub, zpub/vpub, and the
46
+ corresponding multisig prefixes
47
+
48
+ Only non-hardened children can be derived from an extended public key. Export
49
+ an account-level public key from the signing wallet before using this library.
50
+
51
+ ## Install
52
+
53
+ Install the latest release from PyPI:
54
+
55
+ ```bash
56
+ python -m pip install bitwalkit
57
+ ```
58
+
59
+ For development, clone and install from the repository root:
60
+
61
+ ```bash
62
+ git clone https://github.com/elmeriniemela/bitwalkit.git
63
+ cd bitwalkit
64
+ python -m pip install -e '.[dev]'
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ```python
70
+ import bitwalkit as bw
71
+
72
+ # The SLIP-0132 prefix selects the default script type.
73
+ account = bw.Account("zpub6r...")
74
+ first_receive = account.receive_address(0)
75
+ first_change = account.change_address(0)
76
+ gap_limit = account.addresses(change=0, count=20)
77
+
78
+ # Pass script_type explicitly when xpub alone is ambiguous, as with BIP86.
79
+ taproot = bw.Account("xpub6B...", script_type="p2tr")
80
+
81
+ # Multisig derives the same change/index path for every cosigner.
82
+ multisig = bw.MultisigAccount(
83
+ ["Zpub...", "Zpub...", "Zpub..."],
84
+ m=2,
85
+ script_type="p2wsh",
86
+ )
87
+
88
+ # Query public chain state by address.
89
+ chain = bw.ChainQuery("electrum.example.com", port=50002, use_ssl=True)
90
+ balance = chain.get_balance(first_receive)
91
+ utxos = chain.get_utxos(first_receive)
92
+ history = chain.get_history(first_receive)
93
+ balances = chain.get_balances(gap_limit)
94
+
95
+ # Optional direct Bitcoin Core RPC access.
96
+ node = bw.NodeRPC("http://127.0.0.1:8332", "rpcuser", "rpcpassword")
97
+ height = node.getblockcount()
98
+ ```
99
+
100
+ Network access is performed only when calling `ChainQuery` or `NodeRPC`.
101
+ Address derivation and encoding are fully local.
102
+
103
+ ## Tests
104
+
105
+ ```bash
106
+ python -m pytest
107
+ python -m pyright
108
+ ```
109
+
110
+ The suite covers malformed input handling, BIP32 public derivation, address
111
+ encoding, BIP44/49/84/86 derivation, BIP341 taproot vectors, multisig,
112
+ Electrum protocol behavior, and Bitcoin Core JSON-RPC response handling.
113
+
114
+ ## License
115
+
116
+ MIT. See [COPYING](COPYING).
@@ -0,0 +1,99 @@
1
+ # bitwalkit — Watch-only Bitcoin wallet toolkit
2
+
3
+ `bitwalkit` is a standalone, dependency-free Python toolkit for deriving and
4
+ monitoring Bitcoin watch-only wallets. It handles public wallet data only: the
5
+ library does not accept private extended keys, hold secrets, or sign
6
+ transactions.
7
+
8
+ Its public API covers three tasks:
9
+
10
+ 1. Derive single-signature and multisignature addresses from extended public
11
+ keys with `Account` and `MultisigAccount`.
12
+ 2. Query address balances, UTXOs, and transaction history through an Electrum
13
+ server with `ChainQuery`.
14
+ 3. Call a Bitcoin Core node over JSON-RPC with `NodeRPC`.
15
+
16
+ The runtime uses only the Python standard library. A small, public-key-only
17
+ secp256k1 implementation validates compressed public keys, derives
18
+ non-hardened BIP32 child public keys, and applies the BIP341/BIP86 Taproot
19
+ output-key tweak. It is not suitable for private-key handling or transaction
20
+ signing. There are no packages to install at runtime.
21
+
22
+ ## Supported wallets
23
+
24
+ - Single-key P2PKH (BIP44), P2SH-P2WPKH (BIP49), P2WPKH (BIP84), and P2TR
25
+ (BIP86)
26
+ - P2WSH and P2SH-P2WSH multisig (BIP48), with optional BIP67 key sorting
27
+ - Mainnet, testnet, and regtest addresses
28
+ - BIP32 and SLIP-0132 public prefixes: xpub/tpub, ypub/upub, zpub/vpub, and the
29
+ corresponding multisig prefixes
30
+
31
+ Only non-hardened children can be derived from an extended public key. Export
32
+ an account-level public key from the signing wallet before using this library.
33
+
34
+ ## Install
35
+
36
+ Install the latest release from PyPI:
37
+
38
+ ```bash
39
+ python -m pip install bitwalkit
40
+ ```
41
+
42
+ For development, clone and install from the repository root:
43
+
44
+ ```bash
45
+ git clone https://github.com/elmeriniemela/bitwalkit.git
46
+ cd bitwalkit
47
+ python -m pip install -e '.[dev]'
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ```python
53
+ import bitwalkit as bw
54
+
55
+ # The SLIP-0132 prefix selects the default script type.
56
+ account = bw.Account("zpub6r...")
57
+ first_receive = account.receive_address(0)
58
+ first_change = account.change_address(0)
59
+ gap_limit = account.addresses(change=0, count=20)
60
+
61
+ # Pass script_type explicitly when xpub alone is ambiguous, as with BIP86.
62
+ taproot = bw.Account("xpub6B...", script_type="p2tr")
63
+
64
+ # Multisig derives the same change/index path for every cosigner.
65
+ multisig = bw.MultisigAccount(
66
+ ["Zpub...", "Zpub...", "Zpub..."],
67
+ m=2,
68
+ script_type="p2wsh",
69
+ )
70
+
71
+ # Query public chain state by address.
72
+ chain = bw.ChainQuery("electrum.example.com", port=50002, use_ssl=True)
73
+ balance = chain.get_balance(first_receive)
74
+ utxos = chain.get_utxos(first_receive)
75
+ history = chain.get_history(first_receive)
76
+ balances = chain.get_balances(gap_limit)
77
+
78
+ # Optional direct Bitcoin Core RPC access.
79
+ node = bw.NodeRPC("http://127.0.0.1:8332", "rpcuser", "rpcpassword")
80
+ height = node.getblockcount()
81
+ ```
82
+
83
+ Network access is performed only when calling `ChainQuery` or `NodeRPC`.
84
+ Address derivation and encoding are fully local.
85
+
86
+ ## Tests
87
+
88
+ ```bash
89
+ python -m pytest
90
+ python -m pyright
91
+ ```
92
+
93
+ The suite covers malformed input handling, BIP32 public derivation, address
94
+ encoding, BIP44/49/84/86 derivation, BIP341 taproot vectors, multisig,
95
+ Electrum protocol behavior, and Bitcoin Core JSON-RPC response handling.
96
+
97
+ ## License
98
+
99
+ MIT. See [COPYING](COPYING).
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bitwalkit"
7
+ version = "0.1.0"
8
+ description = "Standalone watch-only Bitcoin wallet derivation and monitoring toolkit"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [{ name = "Elmeri Niemelä" }]
13
+ keywords = ["bitcoin", "rpc", "bip32", "electrum", "wallet", "watch-only"]
14
+ dependencies = []
15
+
16
+ [project.optional-dependencies]
17
+ dev = [
18
+ "build>=1.3",
19
+ "pyright>=1.1.400",
20
+ "pytest>=8",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/elmeriniemela/bitwalkit"
25
+
26
+ [tool.setuptools]
27
+ package-dir = { "" = "src" }
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,74 @@
1
+ """bitwalkit -- Bitcoin Wallet Toolkit.
2
+
3
+ Three dependency-free capabilities:
4
+
5
+ * :class:`NodeRPC` -- call a Bitcoin Core node over JSON-RPC.
6
+ * :class:`Account` / :class:`MultisigAccount` -- watch-only HD address
7
+ derivation from master/account extended public keys (xpub/ypub/zpub/...).
8
+ * :class:`ChainQuery` -- fetch an address's balance / UTXOs / history (backed
9
+ by an Electrum server, but the caller only ever deals in addresses).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from .address import (
15
+ address_from_pubkey,
16
+ address_from_script,
17
+ address_to_script,
18
+ address_to_scripthash,
19
+ p2ms_script,
20
+ script_to_scripthash,
21
+ )
22
+ from .bip32 import ExtendedKey
23
+ from .chain import Balance, ChainQuery, HistoryEntry, Utxo
24
+ from .descriptor import descriptor_checksum
25
+ from .encoding import (
26
+ base58check_decode,
27
+ base58check_encode,
28
+ bech32_decode,
29
+ bech32_encode,
30
+ )
31
+ from .errors import (
32
+ BitwalkitError,
33
+ ConnectionError,
34
+ DerivationError,
35
+ EncodingError,
36
+ RpcError,
37
+ )
38
+ from .hd import Account, MultisigAccount
39
+ from .rpc import NodeRPC
40
+
41
+ __version__ = "0.1.0"
42
+
43
+ __all__ = [
44
+ "__version__",
45
+ # rpc
46
+ "NodeRPC",
47
+ # derivation
48
+ "ExtendedKey",
49
+ "Account",
50
+ "MultisigAccount",
51
+ "descriptor_checksum",
52
+ # balances
53
+ "ChainQuery",
54
+ "Balance",
55
+ "Utxo",
56
+ "HistoryEntry",
57
+ # address / encoding helpers
58
+ "address_from_pubkey",
59
+ "address_from_script",
60
+ "address_to_script",
61
+ "address_to_scripthash",
62
+ "script_to_scripthash",
63
+ "p2ms_script",
64
+ "base58check_encode",
65
+ "base58check_decode",
66
+ "bech32_encode",
67
+ "bech32_decode",
68
+ # errors
69
+ "BitwalkitError",
70
+ "RpcError",
71
+ "ConnectionError",
72
+ "DerivationError",
73
+ "EncodingError",
74
+ ]
@@ -0,0 +1,136 @@
1
+ """Internal ElectrumX client: newline-delimited JSON-RPC 2.0 over TCP/SSL.
2
+
3
+ This is a private module -- callers use :class:`bitwalkit.chain.ChainQuery`,
4
+ which speaks in addresses and hides scripthashes and Electrum method names.
5
+ The client supports TCP or TLS connections, configurable timeouts, and batch
6
+ requests.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import socket
13
+ import ssl
14
+
15
+ from .errors import ConnectionError, RpcError
16
+
17
+ __all__ = ["ElectrumClient"]
18
+
19
+
20
+ class ElectrumClient:
21
+ """A short-lived connection to an ElectrumX server."""
22
+
23
+ def __init__(self, host: str, port: int, use_ssl: bool = False, timeout: float = 10) -> None:
24
+ self.host = host
25
+ self.port = port
26
+ self.use_ssl = use_ssl
27
+ self.timeout = timeout
28
+ self._sock: socket.socket | None = None
29
+ self._buf = b""
30
+
31
+ # -- connection -------------------------------------------------------- #
32
+
33
+ def connect(self) -> "ElectrumClient":
34
+ if self._sock is not None:
35
+ return self
36
+ try:
37
+ sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
38
+ if self.use_ssl:
39
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
40
+ ctx.check_hostname = False
41
+ ctx.verify_mode = ssl.CERT_NONE
42
+ sock = ctx.wrap_socket(sock, server_hostname=self.host)
43
+ sock.settimeout(self.timeout)
44
+ except (OSError, ssl.SSLError) as exc:
45
+ raise ConnectionError(
46
+ f"unable to connect to Electrum server {self.host}:{self.port}: {exc}"
47
+ ) from exc
48
+ self._sock = sock
49
+ return self
50
+
51
+ def close(self) -> None:
52
+ if self._sock is not None:
53
+ try:
54
+ self._sock.close()
55
+ finally:
56
+ self._sock = None
57
+ self._buf = b""
58
+
59
+ def __enter__(self) -> "ElectrumClient":
60
+ return self.connect()
61
+
62
+ def __exit__(self, *exc) -> None:
63
+ self.close()
64
+
65
+ # -- transport --------------------------------------------------------- #
66
+
67
+ def _send(self, obj) -> None:
68
+ if self._sock is None:
69
+ self.connect()
70
+ assert self._sock is not None
71
+ try:
72
+ self._sock.sendall(json.dumps(obj).encode() + b"\n")
73
+ except OSError as exc:
74
+ raise ConnectionError(f"Electrum send failed: {exc}") from exc
75
+
76
+ def _recv_line(self) -> bytes:
77
+ assert self._sock is not None
78
+ while b"\n" not in self._buf:
79
+ try:
80
+ chunk = self._sock.recv(4096)
81
+ except socket.timeout as exc:
82
+ raise ConnectionError("Electrum server timed out") from exc
83
+ except OSError as exc:
84
+ raise ConnectionError(f"Electrum recv failed: {exc}") from exc
85
+ if not chunk:
86
+ raise ConnectionError("Electrum server closed the connection")
87
+ self._buf += chunk
88
+ line, self._buf = self._buf.split(b"\n", 1)
89
+ return line
90
+
91
+ @staticmethod
92
+ def _result(item: dict):
93
+ if not isinstance(item, dict):
94
+ raise RpcError(f"invalid Electrum response: {item!r}")
95
+ error = item.get("error")
96
+ if error:
97
+ if isinstance(error, dict):
98
+ raise RpcError(error.get("message", "Electrum error"), error.get("code"))
99
+ raise RpcError(str(error))
100
+ if "result" not in item:
101
+ raise RpcError(f"Electrum response has no result: {item!r}")
102
+ return item["result"]
103
+
104
+ def _receive(self):
105
+ try:
106
+ return json.loads(self._recv_line())
107
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
108
+ raise RpcError("invalid JSON from Electrum server") from exc
109
+
110
+ # -- public calls ------------------------------------------------------ #
111
+
112
+ def call(self, method: str, params: list):
113
+ self._send({"jsonrpc": "2.0", "id": 0, "method": method, "params": params})
114
+ data = self._receive()
115
+ if not isinstance(data, dict) or data.get("id") != 0:
116
+ raise RpcError(f"mismatched Electrum response: {data!r}")
117
+ return self._result(data)
118
+
119
+ def batch(self, method: str, params_list: list[list]) -> list:
120
+ """Call ``method`` once per entry in ``params_list``; results stay in order."""
121
+ if not params_list:
122
+ return []
123
+ reqs = [
124
+ {"jsonrpc": "2.0", "id": i, "method": method, "params": params}
125
+ for i, params in enumerate(params_list)
126
+ ]
127
+ self._send(reqs)
128
+ data = self._receive()
129
+ if not isinstance(data, list) or len(data) != len(params_list):
130
+ raise RpcError("malformed Electrum batch response")
131
+ if any(not isinstance(item, dict) or "id" not in item for item in data):
132
+ raise RpcError("malformed Electrum batch response")
133
+ by_id = {item["id"]: item for item in data}
134
+ if set(by_id) != set(range(len(params_list))):
135
+ raise RpcError("mismatched Electrum batch response ids")
136
+ return [self._result(by_id[i]) for i in range(len(params_list))]
@@ -0,0 +1,126 @@
1
+ """Minimal secp256k1 public-key arithmetic used by bitwalkit.
2
+
3
+ The implementation is intentionally limited to parsing, serializing, adding,
4
+ and multiplying public curve points. It is variable-time and must not be used
5
+ with private or otherwise secret scalars.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import ClassVar
12
+
13
+ __all__ = ["G", "GE"]
14
+
15
+ _FIELD = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
16
+ _ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
17
+ _GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
18
+ _GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class GE:
23
+ """A secp256k1 curve point, or infinity when coordinates are absent."""
24
+
25
+ x: int | None = None
26
+ y: int | None = None
27
+
28
+ ORDER: ClassVar[int] = _ORDER
29
+
30
+ def __post_init__(self) -> None:
31
+ if self.x is None or self.y is None:
32
+ if self.x is not None or self.y is not None:
33
+ raise ValueError("both point coordinates must be present")
34
+ return
35
+ if not (0 <= self.x < _FIELD and 0 <= self.y < _FIELD):
36
+ raise ValueError("point coordinate out of range")
37
+ if (self.y * self.y - self.x * self.x * self.x - 7) % _FIELD:
38
+ raise ValueError("point is not on secp256k1")
39
+
40
+ @property
41
+ def infinity(self) -> bool:
42
+ """Whether this is the point at infinity."""
43
+ return self.x is None
44
+
45
+ def __add__(self, other: object) -> GE:
46
+ """Add two curve points."""
47
+ if not isinstance(other, GE):
48
+ return NotImplemented
49
+ if self.infinity:
50
+ return other
51
+ if other.infinity:
52
+ return self
53
+
54
+ x1, y1, x2, y2 = self.x, self.y, other.x, other.y
55
+ assert x1 is not None and y1 is not None
56
+ assert x2 is not None and y2 is not None
57
+
58
+ if x1 == x2:
59
+ if y1 != y2 or y1 == 0:
60
+ return GE()
61
+ numerator = 3 * x1 * x1
62
+ denominator = 2 * y1
63
+ else:
64
+ numerator = y2 - y1
65
+ denominator = x2 - x1
66
+
67
+ slope = numerator * pow(denominator % _FIELD, -1, _FIELD) % _FIELD
68
+ x3 = (slope * slope - x1 - x2) % _FIELD
69
+ y3 = (slope * (x1 - x3) - y1) % _FIELD
70
+ return GE(x3, y3)
71
+
72
+ def __rmul__(self, scalar: object) -> GE:
73
+ """Multiply this point by an integer using double-and-add."""
74
+ if not isinstance(scalar, int):
75
+ return NotImplemented
76
+
77
+ scalar %= self.ORDER
78
+ result = GE()
79
+ addend = self
80
+ while scalar:
81
+ if scalar & 1:
82
+ result = result + addend
83
+ addend = addend + addend
84
+ scalar >>= 1
85
+ return result
86
+
87
+ @classmethod
88
+ def lift_x(cls, x: int) -> GE:
89
+ """Lift an x-coordinate to the curve point whose y-coordinate is even."""
90
+ if not isinstance(x, int) or not 0 <= x < _FIELD:
91
+ raise ValueError("x-coordinate out of range")
92
+ y_squared = (pow(x, 3, _FIELD) + 7) % _FIELD
93
+ y = pow(y_squared, (_FIELD + 1) // 4, _FIELD)
94
+ if y * y % _FIELD != y_squared:
95
+ raise ValueError("x-coordinate is not on secp256k1")
96
+ if y & 1:
97
+ y = _FIELD - y
98
+ return cls(x, y)
99
+
100
+ @classmethod
101
+ def from_bytes_compressed(cls, encoded: bytes) -> GE:
102
+ """Parse a 33-byte compressed public key."""
103
+ if len(encoded) != 33 or encoded[0] not in (2, 3):
104
+ raise ValueError("invalid compressed public key")
105
+ point = cls.lift_x(int.from_bytes(encoded[1:], "big"))
106
+ assert point.x is not None and point.y is not None
107
+ if (point.y & 1) != (encoded[0] & 1):
108
+ return cls(point.x, _FIELD - point.y)
109
+ return point
110
+
111
+ def to_bytes_compressed(self) -> bytes:
112
+ """Serialize a finite point as a 33-byte compressed public key."""
113
+ if self.infinity:
114
+ raise ValueError("cannot serialize the point at infinity")
115
+ assert self.x is not None and self.y is not None
116
+ return bytes([2 | (self.y & 1)]) + self.x.to_bytes(32, "big")
117
+
118
+ def to_bytes_xonly(self) -> bytes:
119
+ """Serialize the x-coordinate of a finite point."""
120
+ if self.infinity:
121
+ raise ValueError("cannot serialize the point at infinity")
122
+ assert self.x is not None
123
+ return self.x.to_bytes(32, "big")
124
+
125
+
126
+ G = GE(_GX, _GY)