diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index ecb6a5af..24e194d4 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -24,6 +24,7 @@ class DecodeQR: self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder self.base64_qr = Base64DecodeQR() # Single Segments Base64 self.base43_qr = Base43DecodeQR() # Single Segment Base43 + self.address_qr = BitcoinAddressQR() # Single Segment bitcoin address self.wordlist = None for key, value in kwargs.items(): @@ -108,6 +109,12 @@ class DecodeQR: if rt == DecodeQRStatus.COMPLETE: self.complete = True return rt + + elif self.qr_type == QRType.BITCOINADDRESSQR: + rt = self.address_qr.add(qr_str, QRType.BITCOINADDRESSQR) + if rt == DecodeQRStatus.COMPLETE: + self.complete = True + return rt else: return DecodeQRStatus.INVALID @@ -150,6 +157,12 @@ class DecodeQR: def getSeedPhrase(self): return self.seedqr.getSeedPhrase() + + def getAddress(self): + return self.address_qr.getAddress() + + def getAddressType(self): + return self.address_qr.getAddressType() def getPercentComplete(self) -> int: if self.qr_type == QRType.PSBTUR2: @@ -192,6 +205,11 @@ class DecodeQR: if self.qr_type in (QRType.SEEDSSQR, QRType.SEEDUR2, QRType.SEEDMNEMONIC, QRType.SEED4LETTERMNEMONIC): return True return False + + def isAddress(self): + if self.qr_type in (QRType.BITCOINADDRESSQR): + return True + return False def qrType(self): return self.qr_type @@ -220,6 +238,10 @@ class DecodeQR: elif DecodeQR.isBase64PSBT(s): return QRType.PSBTBASE64 + # Bitcoin Address + if DecodeQR.isBitcoinAddress(s): + return QRType.BITCOINADDRESSQR + _4LETTER_WORDLIST = [word[:4].strip() for word in wordlist] # Seed @@ -297,6 +319,16 @@ class DecodeQR: result.extend(b'\x00' * nPad) result.reverse() return bytes(result) + + @staticmethod + def isBitcoinAddress(s): + + if re.search(r'^bitcoin\:.*', s, re.IGNORECASE): + return True + elif re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s): + return True + else: + return False ### ### SpecterDecodePSBTQR Class @@ -618,6 +650,65 @@ class SeedQR: return True return False +### +### BitcoinAddressQR Class +### Purpose: used in DecodeQR to decode single frame representing a bitcoin address +### + +class BitcoinAddressQR: + + def __init__(self, wordlist=None): + self.total_segments = 1 + self.collected_segments = 0 + self.complete = False + self.address = None + self.address_type = None + + def add(self, segment, qr_type=QRType.BITCOINADDRESSQR): + + r = re.search(r'((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})', segment) + if r != None: + self.address = r.group(1) + + if re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address) != None: + self.complete = True + self.collected_segments = 1 + + # get address type + r = re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address) + if r != None: + r = r.group(2) + + if r == "1": + self.address_type = "P2PKH-main" # Legacy + elif r == "m" or r == "n": + self.address_type = "P2PKH-test" # Legacy + elif r == "3": + self.address_type = "P2SH-main" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH) + elif r == "2": + self.address_type = "P2SH-test" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH) + elif r == "bc1": + self.address_type = "Bech32-main" + elif r == "tb1": + self.address_type = "Bech32-test" + + return DecodeQRStatus.COMPLETE + + return DecodeQRStatus.INVALID + + def getAddress(self): + if self.address != None: + return self.address + return None + + def getAddressType(self): + if self.address != None: + if self.address_type != None: + return self.address_type + else: + return "Unknown" + return None + ### ### DecodeQRStatus Class IntEum ### Purpose: used in DecodeQR to communicate status of adding qr frame/segment diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index a3c2490d..0bb7919e 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -17,4 +17,5 @@ class QRType(IntEnum): SEED4LETTERMNEMONIC = 9 XPUBQR = 20 SPECTERXPUBQR = 21 + BITCOINADDRESSQR = 30 INVALID = 100 \ No newline at end of file diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 0b9d2840..98ba3b1b 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -333,7 +333,76 @@ def test_short_4_letter_mnemonic_qr(): assert d.isComplete() == True assert d.getSeedPhrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"] - +def test_bitcoin_address(): + + bad1 = "loremipsum" + bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae" + bad3 = "121802020768124106400009195602431595117715840445" + + legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY" + legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe" + + main_bech32_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + test_bech32_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a" + + main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX" + test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm" + + main_bech32_address2 = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=12000" + main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(bad1) + + assert d.qrType() == QRType.INVALID + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(legacy_address1) + + assert d.getAddress() == legacy_address1 + assert d.getAddressType() == "P2PKH-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(legacy_address2) + + assert d.getAddress() == legacy_address2 + assert d.getAddressType() == "P2PKH-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_bech32_address) + + assert d.getAddress() == main_bech32_address + assert d.getAddressType() == "Bech32-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(test_bech32_address) + + assert d.getAddress() == test_bech32_address + assert d.getAddressType() == "Bech32-test" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_nested_segwit_address) + + assert d.getAddress() == main_nested_segwit_address + assert d.getAddressType() == "P2SH-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(test_nested_segwit_address) + + assert d.getAddress() == test_nested_segwit_address + assert d.getAddressType() == "P2SH-test" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_bech32_address2) + + assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + assert d.getAddressType() == "Bech32-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_bech32_address3) + + assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + assert d.getAddressType() == "Bech32-main"