diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..018cf91b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "urtypes"] + path = src/vendor/urtypes + url = https://github.com/jreesun/urtypes.git diff --git a/README.md b/README.md index 624c2ed7..7c29b1d6 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,14 @@ mkvirtualenv --python=python3 seedsigner-env Now download the SeedSigner code: ``` -git clone https://github.com/SeedSigner/seedsigner +git clone --recursive https://github.com/SeedSigner/seedsigner +``` + +If you have already cloned in the past and just need to init/pull submodules: +``` +cd seedsigner +git submodule update --init +cd .. ``` Install the necessary Python dependencies: diff --git a/requirements.txt b/requirements.txt index 702fce85..baf1501c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ qrcode==6.1 RPi.GPIO==0.7.0 six==1.16.0 spidev==3.5 +-e src/vendor/urtypes \ No newline at end of file diff --git a/src/default_settings.ini b/src/default_settings.ini index 2aeffa0d..f7bbdf59 100644 --- a/src/default_settings.ini +++ b/src/default_settings.ini @@ -5,7 +5,7 @@ persistent_settings = False [display] text_color = ORANGE -qr_background_color = FFFFFF +qr_background_color = 555555 camera_rotation = 0 [wallet] diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 07f90b9c..5f1e2462 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -10,6 +10,8 @@ from seedsigner.helpers.ur2.ur_decoder import URDecoder from seedsigner.helpers.bcur import (cbor_decode, bc32decode) from seedsigner.models import QRType, Seed +from urtypes.crypto import PSBT as UR_PSBT + ### ### DecodeQR Class ### Purpose: used to process images or string data from animated qr codes with psbt data to create binary/base64 psbt @@ -141,7 +143,8 @@ class DecodeQR: if self.complete: if self.qr_type == QRType.PSBTUR2: cbor = self.ur_decoder.result_message().cbor - return cbor_decode(cbor) + return UR_PSBT.from_cbor(cbor).data + # return cbor_decode(cbor) elif self.qr_type == QRType.PSBTSPECTER: return self.specter_qr.getData() elif self.qr_type == QRType.PSBTURLEGACY: diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 394113e2..1a1696a4 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -9,6 +9,9 @@ from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode) from seedsigner.helpers.qr import QR from seedsigner.models import Seed, QRType, EncodeQRDensity +from urtypes.crypto import PSBT as UR_PSBT +from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP + ### ### EncodeQR Class ### Purpose: used to encode psbt for displaying as qr image @@ -63,6 +66,8 @@ class EncodeQR: self.encoder = UREncodePSBTQR(self.psbt, self.qr_density) elif self.qr_type == QRType.SEEDSSQR: self.encoder = SeedSSQR(self.seed_phrase, self.wordlist) + elif self.qr_type == QRType.URXPUBQR: + self.encoder = URXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.qr_density, self.wordlist) elif self.qr_type == QRType.XPUBQR: self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.wordlist) elif self.qr_type == QRType.SPECTERXPUBQR: @@ -100,10 +105,8 @@ class UREncodePSBTQR: def __init__(self, p, qr_density): self.psbt = p self.qr_max_fragement_size = 20 - - cbor_encoder = CBOREncoder() - cbor_encoder.encodeBytes(self.psbt.serialize()) - qr_ur_bytes = UR("crypto-psbt", cbor_encoder.get_bytes()) + + qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor()) if qr_density == EncodeQRDensity.LOW: self.qr_max_fragement_size = 10 @@ -228,7 +231,6 @@ class XPubQR: raise Exception('Wordlist Required') version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[network]) - print(self.derivation) self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist=self.wordlist) self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[network]["xprv"]) self.fingerprint = self.root.child(0).fingerprint @@ -313,4 +315,79 @@ class SpecterXPubQR(XPubQR): return len(self.parts) def isComplete(self): - return self.sent_complete \ No newline at end of file + return self.sent_complete + +class URXPubQR(XPubQR): + + def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist): + + XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, wordlist) + + if qr_density == EncodeQRDensity.LOW: + self.qr_max_fragement_size = 10 + elif qr_density == EncodeQRDensity.MEDIUM: + self.qr_max_fragement_size = 30 + elif qr_density == EncodeQRDensity.HIGH: + self.qr_max_fragement_size = 120 + + def derivation_to_keypath(path: str) -> list: + arr = path.split("/") + if arr[0] == "m": + arr = arr[1:] + if len(arr) == 0: + return Keypath([],self.root.my_fingerprint, None) + if arr[-1] == "": + # trailing slash + arr = arr[:-1] + + for i, e in enumerate(arr): + if e[-1] == "h" or e[-1] == "'": + arr[i] = PathComponent(int(e[:-1]), True) + else: + arr[i] = PathComponent(int(e), False) + + return Keypath(arr, self.root.my_fingerprint, len(arr)) + + origin = derivation_to_keypath(derivation) + + self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(), + 'chain_code': self.xpub.chain_code, + 'origin': origin, + 'parent_fingerprint': self.xpub.fingerprint}) + + ur_outputs = [] + + if len(origin.components) > 0: + if origin.components[0].index == 84: # Native Single Sig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + elif origin.components[0].index == 49: # Nested Single Sig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + elif origin.components[0].index == 48: # Multisig + if len(origin.components) >= 4: + if origin.components[3].index == 2: # Native Multisig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + elif origin.components[3].index == 1: # Nested Multisig + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + + # If empty, add all script types + if len(ur_outputs) == 0: + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[403]],self.ur_hdkey)) + + ur_account = Account(self.root.my_fingerprint, ur_outputs) + + qr_ur_bytes = UR("crypto-account", ur_account.to_cbor()) + + self.ur2_encode = UREncoder(qr_ur_bytes, self.qr_max_fragement_size, 0) + + def seqLen(self): + return self.ur2_encode.fountain_encoder.seq_len() + + def nextPart(self) -> str: + return self.ur2_encode.next_part().upper() + + def isComplete(self): + return self.ur2_encode.is_complete() \ No newline at end of file diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index 984e5d4c..a963d737 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -17,6 +17,7 @@ class QRType(IntEnum): SEED4LETTERMNEMONIC = 9 XPUBQR = 20 SPECTERXPUBQR = 21 + URXPUBQR = 22 BITCOINADDRESSQR = 30 SPECTERWALLETQR = 40 URWALLETQR = 41 diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 586f3040..26c80bec 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -203,8 +203,10 @@ class Settings(Singleton): def getXPubType(software): if software == "Specter Desktop": return QRType.SPECTERXPUBQR - else: + elif software == "Blue Wallet": return QRType.XPUBQR + else: + return QRType.URXPUBQR @property def qr_density_name(self): diff --git a/src/settings.ini b/src/settings.ini index 2aeffa0d..f7bbdf59 100644 --- a/src/settings.ini +++ b/src/settings.ini @@ -5,7 +5,7 @@ persistent_settings = False [display] text_color = ORANGE -qr_background_color = FFFFFF +qr_background_color = 555555 camera_rotation = 0 [wallet] diff --git a/src/vendor/urtypes b/src/vendor/urtypes new file mode 160000 index 00000000..8d643aa7 --- /dev/null +++ b/src/vendor/urtypes @@ -0,0 +1 @@ +Subproject commit 8d643aa7f5bf1e3295a5e176afcfb5b6240d66e6 diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index c5b4048b..a2882306 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -85,4 +85,14 @@ def test_specter_xpub_qr(): assert e.nextPart() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J" assert e.nextPart() == "p4of4 sZsfdBzsK3VUC5" +def test_ur_xpub_qr(): + + mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.URXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.MEDIUM, wordlist=bip39.WORDLIST) + + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSJKCYYNRDRTYAHDCAOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNRTAAOLAH" + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSJKCYYNRDRTYAHDCASNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYQDLAWNGL" + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSJKCYYNRDRTYAHDCARSTDSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDSEPDJLAT" + assert e.nextPart() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSJKCYYNRDRTYAHDCAAMTAADDYOEADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAYCYIOREKKJKAECADRRKIE"