Created
October 6, 2025 11:33
-
-
Save ferib/71cfeb41ce6cc82ae28c520e8e27d18d to your computer and use it in GitHub Desktop.
Converts pubkey to Nostr address
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| % python3 npub1.py 0350dbc4dfd2313112fa6d1ac3effed7d1428b3c8dcc7357c0b84bac8f356a1894 | |
| Compressed BTC pubkey (hex): 0350dbc4dfd2313112fa6d1ac3effed7d1428b3c8dcc7357c0b84bac8f356a1894 | |
| Nostr x-only pubkey (hex) : 50dbc4dfd2313112fa6d1ac3effed7d1428b3c8dcc7357c0b84bac8f356a1894 | |
| Nostr npub : npub12rdufh7jxyc397ndrtp7llkh69pgk0yde3e40s9cfwkg7dt2rz2qjdzd2t | |
| """ | |
| import argparse | |
| import binascii | |
| import sys | |
| # ---------- Bech32 (BIP-173) for NIP-19 ---------- | |
| CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" | |
| def bech32_polymod(values): | |
| GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] | |
| chk = 1 | |
| for v in values: | |
| b = chk >> 25 | |
| chk = ((chk & 0x1ffffff) << 5) ^ v | |
| for i in range(5): | |
| if (b >> i) & 1: | |
| chk ^= GENERATORS[i] | |
| return chk | |
| def bech32_hrp_expand(hrp): | |
| return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] | |
| def bech32_create_checksum(hrp, data): | |
| values = bech32_hrp_expand(hrp) + data | |
| polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 | |
| return [(polymod >> 5*(5-i)) & 31 for i in range(6)] | |
| def bech32_encode(hrp, data): | |
| combined = data + bech32_create_checksum(hrp, data) | |
| return hrp + '1' + ''.join([CHARSET[d] for d in combined]) | |
| def convertbits(data, frombits, tobits, pad=True): | |
| acc = 0 | |
| bits = 0 | |
| ret = [] | |
| maxv = (1 << tobits) - 1 | |
| max_acc = (1 << (frombits + tobits - 1)) - 1 | |
| for value in data: | |
| if value < 0 or (value >> frombits): | |
| return None | |
| acc = ((acc << frombits) | value) & max_acc | |
| bits += frombits | |
| while bits >= tobits: | |
| bits -= tobits | |
| ret.append((acc >> bits) & maxv) | |
| if pad: | |
| if bits: | |
| ret.append((acc << (tobits - bits)) & maxv) | |
| elif bits >= frombits or ((acc << (tobits - bits)) & maxv): | |
| return None | |
| return ret | |
| def nip19_encode_npub(pubkey_bytes: bytes) -> str: | |
| """Encode raw 32-byte pubkey into NIP-19 'npub' (Bech32, not Bech32m).""" | |
| data5 = convertbits(list(pubkey_bytes), 8, 5, True) | |
| if data5 is None: | |
| raise ValueError("convertbits failed") | |
| return bech32_encode("npub", data5) | |
| def is_compressed_pubkey(pub: bytes) -> bool: | |
| return len(pub) == 33 and pub[0] in (0x02, 0x03) | |
| def main(): | |
| ap = argparse.ArgumentParser( | |
| description="Convert a compressed Bitcoin pubkey (02/03 + 32 bytes) into a Nostr pubkey (hex x-only and npub)." | |
| ) | |
| ap.add_argument("pubkey_hex", help="Compressed secp256k1 pubkey in hex (starts with 02/03, 33 bytes).") | |
| args = ap.parse_args() | |
| try: | |
| pub = bytes.fromhex(args.pubkey_hex.strip()) | |
| except ValueError: | |
| print("Error: invalid hex for pubkey.", file=sys.stderr) | |
| sys.exit(1) | |
| if not is_compressed_pubkey(pub): | |
| print("Error: pubkey must be 33 bytes and start with 0x02 or 0x03 (compressed).", file=sys.stderr) | |
| sys.exit(1) | |
| # Extract x-only (drop the first byte) | |
| x_only = pub[1:] | |
| x_only_hex = x_only.hex() | |
| npub = nip19_encode_npub(x_only) | |
| print("Compressed BTC pubkey (hex):", args.pubkey_hex.lower()) | |
| print("Nostr x-only pubkey (hex) :", x_only_hex) | |
| print("Nostr npub :", npub) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment