Skip to content

Commit edb5afb

Browse files
committed
Initial implementation
1 parent ad1d65a commit edb5afb

19 files changed

+7347
-0
lines changed

.env-sample

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RPC_HOST = "localhost"
2+
RPC_USER = ""
3+
RPC_PASSWORD = ""
4+
RPC_PORT = ""

btctools/__init__.py

Whitespace-only changes.

btctools/_base58.py

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""
2+
Base 58 conversion utilities
3+
****************************
4+
"""
5+
6+
#
7+
# base58.py
8+
# Original source: git://github.com/joric/brutus.git
9+
# which was forked from git://github.com/samrushing/caesure.git
10+
#
11+
# Distributed under the MIT/X11 software license, see the accompanying
12+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
13+
#
14+
15+
from binascii import hexlify, unhexlify
16+
from typing import List
17+
18+
from .common import hash256
19+
from .errors import BadArgumentError
20+
21+
22+
b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
23+
24+
25+
def encode(b: bytes) -> str:
26+
"""
27+
Encode bytes to a base58-encoded string
28+
29+
:param b: Bytes to encode
30+
:return: Base58 encoded string of ``b``
31+
"""
32+
33+
# Convert big-endian bytes to integer
34+
n: int = int('0x0' + hexlify(b).decode('utf8'), 16)
35+
36+
# Divide that integer into base58
37+
temp: List[str] = []
38+
while n > 0:
39+
n, r = divmod(n, 58)
40+
temp.append(b58_digits[r])
41+
res: str = ''.join(temp[::-1])
42+
43+
# Encode leading zeros as base58 zeros
44+
czero: int = 0
45+
pad: int = 0
46+
for c in b:
47+
if c == czero:
48+
pad += 1
49+
else:
50+
break
51+
return b58_digits[0] * pad + res
52+
53+
def decode(s: str) -> bytes:
54+
"""
55+
Decode a base58-encoding string, returning bytes
56+
57+
:param s: Base48 string to decode
58+
:return: Bytes encoded by ``s``
59+
"""
60+
if not s:
61+
return b''
62+
63+
# Convert the string to an integer
64+
n: int = 0
65+
for c in s:
66+
n *= 58
67+
if c not in b58_digits:
68+
raise BadArgumentError('Character %r is not a valid base58 character' % c)
69+
digit = b58_digits.index(c)
70+
n += digit
71+
72+
# Convert the integer to bytes
73+
h: str = '%x' % n
74+
if len(h) % 2:
75+
h = '0' + h
76+
res = unhexlify(h.encode('utf8'))
77+
78+
# Add padding back.
79+
pad = 0
80+
for c in s[:-1]:
81+
if c == b58_digits[0]:
82+
pad += 1
83+
else:
84+
break
85+
return b'\x00' * pad + res
86+
87+
def decode_check(s: str) -> bytes:
88+
"""
89+
Decode a Base58Check encoded string, returning bytes
90+
91+
:param s: Base58 string to decode
92+
:return: Bytes encoded by ``s``
93+
"""
94+
data = decode(s)
95+
payload = data[:-4]
96+
checksum = data[-4:]
97+
calc_checksum = hash256(payload)
98+
if checksum != calc_checksum:
99+
raise ValueError("Invalid checksum")
100+
return payload
101+
102+
def encode_check(b: bytes) -> str:
103+
checksum = hash256(b)[0:4]
104+
data = b + checksum
105+
return encode(data)
106+
107+
def get_xpub_fingerprint(s: str) -> bytes:
108+
"""
109+
Get the parent fingerprint from an extended public key
110+
111+
:param s: The extended pubkey
112+
:return: The parent fingerprint bytes
113+
"""
114+
data = decode(s)
115+
fingerprint = data[5:9]
116+
return fingerprint
117+
118+
def get_xpub_fingerprint_hex(xpub: str) -> str:
119+
"""
120+
Get the parent fingerprint as a hex string from an extended public key
121+
122+
:param s: The extended pubkey
123+
:return: The parent fingerprint as a hex string
124+
"""
125+
data = decode(xpub)
126+
fingerprint = data[5:9]
127+
return hexlify(fingerprint).decode()
128+
129+
def to_address(b: bytes, version: bytes) -> str:
130+
"""
131+
Base58 Check Encode the data with the version number.
132+
Used to encode legacy style addresses.
133+
134+
:param b: The data to encode
135+
:param version: The version number to encode with
136+
:return: The Base58 Check Encoded string
137+
"""
138+
data = version + b
139+
checksum = hash256(data)[0:4]
140+
data += checksum
141+
return encode(data)
142+
143+
def xpub_to_pub_hex(xpub: str) -> str:
144+
"""
145+
Get the public key as a string from the extended public key.
146+
147+
:param xpub: The extended pubkey
148+
:return: The pubkey hex string
149+
"""
150+
data = decode(xpub)
151+
pubkey = data[-37:-4]
152+
return hexlify(pubkey).decode()
153+
154+
155+
def xpub_to_xonly_pub_hex(xpub: str) -> str:
156+
"""
157+
Get the public key as a string from the extended public key.
158+
159+
:param xpub: The extended pubkey
160+
:return: The pubkey hex string
161+
"""
162+
data = decode(xpub)
163+
pubkey = data[-36:-4]
164+
return hexlify(pubkey).decode()
165+
166+
167+
def xpub_main_2_test(xpub: str) -> str:
168+
"""
169+
Convert an extended pubkey from mainnet version to testnet version.
170+
171+
:param xpub: The extended pubkey
172+
:return: The extended pubkey re-encoded using testnet version bytes
173+
"""
174+
data = decode(xpub)
175+
test_data = b'\x04\x35\x87\xCF' + data[4:-4]
176+
checksum = hash256(test_data)[0:4]
177+
return encode(test_data + checksum)

btctools/_script.py

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# from https://github.com/bitcoin-core/HWI
2+
3+
"""
4+
Bitcoin Script utilities
5+
************************
6+
"""
7+
8+
from typing import (
9+
Optional,
10+
Sequence,
11+
Tuple,
12+
)
13+
14+
15+
def is_opreturn(script: bytes) -> bool:
16+
"""
17+
Determine whether a script is an OP_RETURN output script.
18+
19+
:param script: The script
20+
:returns: Whether the script is an OP_RETURN output script
21+
"""
22+
return script[0] == 0x6a
23+
24+
25+
def is_p2sh(script: bytes) -> bool:
26+
"""
27+
Determine whether a script is a P2SH output script.
28+
29+
:param script: The script
30+
:returns: Whether the script is a P2SH output script
31+
"""
32+
return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87
33+
34+
35+
def is_p2pkh(script: bytes) -> bool:
36+
"""
37+
Determine whether a script is a P2PKH output script.
38+
39+
:param script: The script
40+
:returns: Whether the script is a P2PKH output script
41+
"""
42+
return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac
43+
44+
45+
def is_p2pk(script: bytes) -> bool:
46+
"""
47+
Determine whether a script is a P2PK output script.
48+
49+
:param script: The script
50+
:returns: Whether the script is a P2PK output script
51+
"""
52+
return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac
53+
54+
55+
def is_witness(script: bytes) -> Tuple[bool, int, bytes]:
56+
"""
57+
Determine whether a script is a segwit output script.
58+
If so, also returns the witness version and witness program.
59+
60+
:param script: The script
61+
:returns: A tuple of a bool indicating whether the script is a segwit output script,
62+
an int representing the witness version,
63+
and the bytes of the witness program.
64+
"""
65+
if len(script) < 4 or len(script) > 42:
66+
return (False, 0, b"")
67+
68+
if script[0] != 0 and (script[0] < 81 or script[0] > 96):
69+
return (False, 0, b"")
70+
71+
if script[1] + 2 == len(script):
72+
return (True, script[0] - 0x50 if script[0] else 0, script[2:])
73+
74+
return (False, 0, b"")
75+
76+
77+
def is_p2wpkh(script: bytes) -> bool:
78+
"""
79+
Determine whether a script is a P2WPKH output script.
80+
81+
:param script: The script
82+
:returns: Whether the script is a P2WPKH output script
83+
"""
84+
is_wit, wit_ver, wit_prog = is_witness(script)
85+
if not is_wit:
86+
return False
87+
elif wit_ver != 0:
88+
return False
89+
return len(wit_prog) == 20
90+
91+
92+
def is_p2wsh(script: bytes) -> bool:
93+
"""
94+
Determine whether a script is a P2WSH output script.
95+
96+
:param script: The script
97+
:returns: Whether the script is a P2WSH output script
98+
"""
99+
is_wit, wit_ver, wit_prog = is_witness(script)
100+
if not is_wit:
101+
return False
102+
elif wit_ver != 0:
103+
return False
104+
return len(wit_prog) == 32
105+
106+
def is_p2tr(script: bytes) -> bool:
107+
"""
108+
Determine whether a script is a P2TR output script.
109+
110+
:param script: The script
111+
:returns: Whether the script is a P2TR output script
112+
"""
113+
is_wit, wit_ver, wit_prog = is_witness(script)
114+
if not is_wit:
115+
return False
116+
elif wit_ver != 1:
117+
return False
118+
return len(wit_prog) == 32
119+
120+
121+
# Only handles up to 15 of 15. Returns None if this script is not a
122+
# multisig script. Returns (m, pubkeys) otherwise.
123+
def parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]:
124+
"""
125+
Determine whether a script is a multisig script. If so, determine the parameters of that multisig.
126+
127+
:param script: The script
128+
:returns: ``None`` if the script is not multisig.
129+
If multisig, returns a tuple of the number of signers required,
130+
and a sequence of public key bytes.
131+
"""
132+
# Get m
133+
m = script[0] - 80
134+
if m < 1 or m > 15:
135+
return None
136+
137+
# Get pubkeys
138+
pubkeys = []
139+
offset = 1
140+
while True:
141+
pubkey_len = script[offset]
142+
if pubkey_len != 33:
143+
break
144+
offset += 1
145+
pubkeys.append(script[offset:offset + 33])
146+
offset += 33
147+
148+
# Check things at the end
149+
n = script[offset] - 80
150+
if n != len(pubkeys):
151+
return None
152+
offset += 1
153+
op_cms = script[offset]
154+
if op_cms != 174:
155+
return None
156+
157+
return (m, pubkeys)

0 commit comments

Comments
 (0)