feat: Loading a Descriptor Sets Wallet Attributes (#802)
What changed, and why it matters
This commit changes how Krux handles wallet descriptors. Instead of requiring users to manually set wallet type (single-sig, multisig, miniscript), network (mainnet/testnet), and address format, Krux now reads these directly from the descriptor. It also removes the old feature that tried to guess these settings from raw xpubs, which could make unsafe assumptions. When a loaded descriptor doesn't match the currently configured wallet, Krux now shows a clear warning and asks the user whether to switch settings automatically. Overall this is a security-hardening and usability improvement, not an introduced vulnerability.
No urgent action required. Reviewers and users should verify that the new mismatch prompt is clear and that the removed xpub-as-descriptor fallback is acceptable for their workflows. Consider whether any downstream tools relied on loading bare xpubs.
Security signals we found
Removes unsafe xpub-to-descriptor assumption logic that could infer wrong BIP purpose/account/network
Adds explicit user prompt before changing wallet policy, script type, or network
Introduces PolicyMismatchWarning to surface mismatches instead of failing silently or with generic ValueError
Validates descriptor-derived network via xpub version bytes
Adds tests covering policy, network, and script-type mismatch accept/decline paths
Evidence from the diff
The patch refactors wallet loading in src/krux/wallet.py and src/krux/pages/home_pages/wallet_descriptor.py. It replaces the AssumptionWarning/allow_assumption mechanism with PolicyMismatchWarning, derives required policy_type, script_type, and network directly from the descriptor, and prompts the user before re-deriving the Key to match. It also removes parse_wallet’s fallback that converted bare xpubs into descriptors via guessed derivations (xpub_data_to_derivation, derivation_to_script_wrapper). The change is defensive: it reduces silent assumption of sensitive wallet parameters and requires explicit user approval for policy/network/script changes.
Changed components
src/krux/wallet.pysrc/krux/pages/home_pages/wallet_descriptor.pysrc/krux/key.pyi18n translation filestests/test_wallet.pytests/pages/home_pages/test_wallet_descriptor.pyInspect captured patch +1137 / −496
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4ed5edf..a202a66 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -105,6 +105,6 @@ jobs:
- name: Build coverage file
run: poetry run pytest --cache-clear --cov src/krux --cov-report xml tests
- name: Upload coverage reports to Codecov with GitHub Action
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 387b261..11e9d4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ This device shares similarities with the WonderMV but stands out with its larger
### New Device Support: WonderK PRO
From the wonderful land of Korea, a new creation arrives: the WonderK PRO. Created by an entrepreneur who loves the Krux project, the WonderK follows in the footsteps of the WonderMV, but boasts a larger 2.8" display! Computer simulator for the WonderK device is also included.
+### Loading a Descriptor Sets Wallet Attributes
+Loading a descriptor is now a quick way to set wallet attributes, as all necessary information is extracted automatically. This eliminates the need for manual configuration and ensures consistency with your coordinator wallet.
+
### Changed BIP39 Passphrase Validation
Krux now displays a warning instead of blocking QR-encoded passphrases that contain non-ASCII characters. Users are encouraged to use QR codes containing only ASCII passphrases or non-ASCII that have already been normalized to NFKD.
diff --git a/docs/getting-started/usage/navigating-the-main-menu.en.md b/docs/getting-started/usage/navigating-the-main-menu.en.md
index 6dc1b60..3857fbc 100644
--- a/docs/getting-started/usage/navigating-the-main-menu.en.md
+++ b/docs/getting-started/usage/navigating-the-main-menu.en.md
@@ -152,6 +152,13 @@ For multisig and miniscript, loading a wallet descriptor is essential to verify
When you select the `Wallet Descriptor` option for the first time, you will be prompted to load via QR code or SD card. After loading, a preview of the descriptor attributes will be displayed for confirmation. We shown each key’s fingerprint, derivation path, and abbreviated XPUB highlighted with a different color.
+Loading a descriptor is also a quick way to set wallet attributes, as all necessary information is extracted automatically:
+- Network (Mainnet or Testnet)
+- Policy type (single-sig, multisig or miniscript)
+- Script type (Native Segwit, Taproot, Nested Segwit, Legacy)
+
+This eliminates the need for manual configuration and ensures consistency with your coordinator wallet.
+
<div style="clear: both"></div>
**Miniscript Descriptors** present an indented view of the miniscript after the keys. When Taproot is used, Krux checks if the internal key is "provably unspendable", meaning funds can only be moved via Tap tree scripts, in which case the internal key is displayed in a disabled color.
diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json
index ad6c3ae..9efac85 100644
--- a/i18n/translations/de-DE.json
+++ b/i18n/translations/de-DE.json
@@ -13,9 +13,7 @@
"12 words": "12 Wörter",
"24 words": "24 Wörter",
"About": "Über",
- "Accept assumption?": "Annahme akzeptieren?",
"Account": "Konto",
- "Account #0 would be assumed": "Konto #0 würde angenommen",
"Account Index": "Kontoindex",
"Add or change wallet passphrase?": "Wallet-Passphrase hinzufügen oder ändern?",
"Additional entropy from camera required for %s": "Zusätzliche Entropie von der Kamera erforderlich für %s",
@@ -40,6 +38,7 @@
"Capture cancelled": "Aufnahme abgebrochen",
"Change": "Change Adresse",
"Change theme and reboot?": "Thema ändern und neu starten?",
+ "Change wallet?": "Geldbörse ändern?",
"Change:": "Change:",
"Changes will last until shutdown.": "Änderungen bleiben bis zum Herunterfahren bestehen.",
"Check SD Card": "Prüfe SD-Karte",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "Mnemonisches XOR",
"Mnemonic and passphrase will be kept.": "Mnemotechnik und Passphrase werden beibehalten.",
"Modified:": "Geändert:",
- "Native Segwit - 84 would be assumed": "Native Segwit - 84 würde angenommen",
"Network": "Netzwerk",
"New Mnemonic": "Neue Mnemonic",
"New firmware detected.": "Neue Firmware erkannt.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Warte auf die Erfassung",
"Wallet": "Wallet",
"Wallet Descriptor": "Wallet-Deskriptor",
+ "Wallet mismatch:": "Geldbörse passt nicht:",
"Wallet output descriptor": "Wallet Ausgabedeskriptor",
"Wallet output descriptor loaded!": "Wallet Ausgabedeskriptor geladen!",
"Wallet output descriptor not found.": "Wallet Ausgabedeskriptor nicht gefunden.",
diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json
index 9c22e7b..ad181ab 100644
--- a/i18n/translations/es-MX.json
+++ b/i18n/translations/es-MX.json
@@ -13,9 +13,7 @@
"12 words": "12 palabras",
"24 words": "24 palabras",
"About": "Nosotros",
- "Accept assumption?": "¿Aceptar la suposición?",
"Account": "Cuenta",
- "Account #0 would be assumed": "Se supondría que la cuenta #0",
"Account Index": "Índice de la cuenta",
"Add or change wallet passphrase?": "¿Añadir o cambiar passphrase de la cartera?",
"Additional entropy from camera required for %s": "Se requiere entropía adicional de la cámara para %s",
@@ -40,6 +38,7 @@
"Capture cancelled": "Captura cancelada",
"Change": "Cambio",
"Change theme and reboot?": "¿Cambiar de tema y reiniciar?",
+ "Change wallet?": "¿Cambiar cartera?",
"Change:": "Cambio:",
"Changes will last until shutdown.": "Los cambios durarán hasta que el dispositivo se apague.",
"Check SD Card": "Revisar Tarjeta SD",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "XOR mnemónico",
"Mnemonic and passphrase will be kept.": "Mnemónico y passphrase se mantendrán.",
"Modified:": "Modificado:",
- "Native Segwit - 84 would be assumed": "Segwit nativo - 84 se supondría",
"Network": "Red",
"New Mnemonic": "Nuevo Mnemónico",
"New firmware detected.": "Nuevo firmware detectado.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Espera la captura",
"Wallet": "Cartera",
"Wallet Descriptor": "Descriptor de Cartera",
+ "Wallet mismatch:": "Cartera no coincide:",
"Wallet output descriptor": "Descriptor de salida de cartera",
"Wallet output descriptor loaded!": "¡Se ha cargado el descriptor de salida de la cartera!",
"Wallet output descriptor not found.": "No se encontró el descriptor de salida de la cartera.",
diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json
index 84ee289..e9be612 100644
--- a/i18n/translations/fr-FR.json
+++ b/i18n/translations/fr-FR.json
@@ -13,9 +13,7 @@
"12 words": "12 mots",
"24 words": "24 mots",
"About": "À propos",
- "Accept assumption?": "Accepter supposition ?",
"Account": "Compte",
- "Account #0 would be assumed": "Le compte n °0 serait supposé",
"Account Index": "Index du compte",
"Add or change wallet passphrase?": "Ajoutez ou modifiez la phrase secrète ?",
"Additional entropy from camera required for %s": "Entropie supplémentaire de la caméra requise pour %s",
@@ -40,6 +38,7 @@
"Capture cancelled": "Capture annulée",
"Change": "Monnaie",
"Change theme and reboot?": "Changer de thème et redémarrer ?",
+ "Change wallet?": "Changer portefeuille?",
"Change:": "La monnaie :",
"Changes will last until shutdown.": "Les modifications dureront jusqu'à l'arrêt.",
"Check SD Card": "Vérifiez la carte SD",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "Mnémonique XOR",
"Mnemonic and passphrase will be kept.": "Mnémonique et phrase secrète seront conservés.",
"Modified:": "Modifié :",
- "Native Segwit - 84 would be assumed": "Native Segwit - 84 serait supposé",
"Network": "Réseau",
"New Mnemonic": "Nouveau Mnémonique",
"New firmware detected.": "Nouveau micrologiciel détecté.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Attendez la capture",
"Wallet": "Portefeuille",
"Wallet Descriptor": "Descripteur de Portefeuille",
+ "Wallet mismatch:": "Portefeuille différent:",
"Wallet output descriptor": "Descripteur de sortie du portefeuille",
"Wallet output descriptor loaded!": "Descripteur de sortie du portefeuille chargé !",
"Wallet output descriptor not found.": "Descripteur de sortie du portefeuille introuvable.",
diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json
index 9986b56..a2dfc29 100644
--- a/i18n/translations/ja-JP.json
+++ b/i18n/translations/ja-JP.json
@@ -13,9 +13,7 @@
"12 words": "12の単語",
"24 words": "24の単語",
"About": "概要",
- "Accept assumption?": "仮定を受け入れますか?",
"Account": "アカウント",
- "Account #0 would be assumed": "アカウント#0は仮定されます",
"Account Index": "アカウントインデックス",
"Add or change wallet passphrase?": "ウォレットのパスフレーズを追加または変更しますか?",
"Additional entropy from camera required for %s": "%sにはカメラからの追加エントロピーが必要です",
@@ -40,6 +38,7 @@
"Capture cancelled": "キャプチャがキャンセルされました",
"Change": "お釣り",
"Change theme and reboot?": "テーマの変更と再起動しますか?",
+ "Change wallet?": "ウォレット変更?",
"Change:": "お釣り:",
"Changes will last until shutdown.": "変更はシャットダウンするまで持続します.",
"Check SD Card": "SDカードを確認する",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "ニーモニックXOR",
"Mnemonic and passphrase will be kept.": "Mnemonicとパスフレーズは保持されます.",
"Modified:": "修正されました:",
- "Native Segwit - 84 would be assumed": "ネイティブSegwit - 84が仮定されます",
"Network": "ネットワーク",
"New Mnemonic": "新しい Mnemonic",
"New firmware detected.": "新しいファームウェアが検出されました.",
@@ -341,6 +339,7 @@
"Wait for the capture": "キャプチャを待ってください",
"Wallet": "ワレット",
"Wallet Descriptor": "ウォレットディスクリプター",
+ "Wallet mismatch:": "ウォレット不一致:",
"Wallet output descriptor": "ウォレット出力ディスクリプター",
"Wallet output descriptor loaded!": "ウォレット出力ディスクリプターがロードされました!",
"Wallet output descriptor not found.": "ウォレット出力ディスクリプターが見つかりません.",
diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json
index 1339988..0124099 100644
--- a/i18n/translations/ko-KR.json
+++ b/i18n/translations/ko-KR.json
@@ -13,9 +13,7 @@
"12 words": "12단어",
"24 words": "24단어",
"About": "기본 정보",
- "Accept assumption?": "가정을 수락하시겠습니까?",
"Account": "계정",
- "Account #0 would be assumed": "계정 #0이 가정됩니다",
"Account Index": "계정 인덱스",
"Add or change wallet passphrase?": "패스프레이즈를 추가하거나 변경하시겠습니까?",
"Additional entropy from camera required for %s": "%s 에 필요한 카메라의 추가 엔트로피",
@@ -40,6 +38,7 @@
"Capture cancelled": "캡처 취소됨",
"Change": "잔돈",
"Change theme and reboot?": "테마를 변경하고 재부팅하시겠습니까?",
+ "Change wallet?": "지갑 변경?",
"Change:": "잔돈:",
"Changes will last until shutdown.": "변경 사항은 종료될때까지 유지됩니다.",
"Check SD Card": "SD카드 확인",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "니모닉 XOR",
"Mnemonic and passphrase will be kept.": "니모닉과 암호는 유지됩니다.",
"Modified:": "수정되었습니다:",
- "Native Segwit - 84 would be assumed": "네이티브 세그윗 - BIP84를 적용합니다",
"Network": "네트워크",
"New Mnemonic": "새로운 니모닉",
"New firmware detected.": "새로운 펌웨어가 감지되었습니다.",
@@ -341,6 +339,7 @@
"Wait for the capture": "캡처될때까지 기다리십시오",
"Wallet": "지갑 설정",
"Wallet Descriptor": "지갑 디스크립터",
+ "Wallet mismatch:": "지갑 불일치:",
"Wallet output descriptor": "지갑 출력 디스크립터",
"Wallet output descriptor loaded!": "지갑 출력 디스크립터가 로드되었습니다!",
"Wallet output descriptor not found.": "지갑 출력 디스크립터를 찾을 수 없습니다.",
diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json
index 83c4518..0595c04 100644
--- a/i18n/translations/nl-NL.json
+++ b/i18n/translations/nl-NL.json
@@ -13,9 +13,7 @@
"12 words": "12 woorden",
"24 words": "24 woorden",
"About": "Over",
- "Accept assumption?": "Aanname accepteren?",
"Account": "Account",
- "Account #0 would be assumed": "Account #0 zou worden aangenomen",
"Account Index": "Accountindex",
"Add or change wallet passphrase?": "Wachtwoordzin voor portemonnee toevoegen of wijzigen?",
"Additional entropy from camera required for %s": "Extra entropie van camera vereist voor %s",
@@ -40,6 +38,7 @@
"Capture cancelled": "Opname geannuleerd",
"Change": "Change",
"Change theme and reboot?": "Thema veranderen en opnieuw opstarten?",
+ "Change wallet?": "Portemonnee wijzigen?",
"Change:": "Wisselgeld:",
"Changes will last until shutdown.": "Wijzigingen blijven van kracht tot afsluiten.",
"Check SD Card": "Controleer SD kaart",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "Mnemonic XOR",
"Mnemonic and passphrase will be kept.": "Geheugensteun en wachtwoord worden bewaard.",
"Modified:": "Aangepast:",
- "Native Segwit - 84 would be assumed": "Native Segwit - 84 zal worden gebruikt",
"Network": "Netwerk",
"New Mnemonic": "Geheugensteun aanmaken",
"New firmware detected.": "Nieuwe firmware gevonden.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Wacht op opname",
"Wallet": "Portemonnee",
"Wallet Descriptor": "Descriptor",
+ "Wallet mismatch:": "Portemonnee onjuist:",
"Wallet output descriptor": "Portemonnee descriptor",
"Wallet output descriptor loaded!": "Portemonnee descriptor geladen!",
"Wallet output descriptor not found.": "Portemonnee descriptor niet gevonden.",
diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json
index c98b1cf..e506f1e 100644
--- a/i18n/translations/pt-BR.json
+++ b/i18n/translations/pt-BR.json
@@ -13,9 +13,7 @@
"12 words": "12 palavras",
"24 words": "24 palavras",
"About": "Sobre",
- "Accept assumption?": "Aceitar suposições?",
"Account": "Conta",
- "Account #0 would be assumed": "A conta #0 seria assumida",
"Account Index": "Índice da Conta",
"Add or change wallet passphrase?": "Adicionar ou alterar a senha da carteira?",
"Additional entropy from camera required for %s": "Entropia adicional da câmera é necessária para %s",
@@ -40,6 +38,7 @@
"Capture cancelled": "Captura cancelada",
"Change": "Troco",
"Change theme and reboot?": "Mudar o tema e reiniciar?",
+ "Change wallet?": "Mudar carteira?",
"Change:": "Troco:",
"Changes will last until shutdown.": "As alterações só durarão até o desligamento.",
"Check SD Card": "Explorar o cartão SD",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "XOR Mnemônico",
"Mnemonic and passphrase will be kept.": "Mnemônico e senha serão mantidos.",
"Modified:": "Alterado:",
- "Native Segwit - 84 would be assumed": "Segwit nativo - 84 seria assumido",
"Network": "Rede",
"New Mnemonic": "Novo Mnemônico",
"New firmware detected.": "Novo firmware detectado.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Aguarde a captura",
"Wallet": "Carteira",
"Wallet Descriptor": "Descritor da Carteira",
+ "Wallet mismatch:": "Carteira diferente:",
"Wallet output descriptor": "Descritor da carteira",
"Wallet output descriptor loaded!": "Descritor da carteira carregado!",
"Wallet output descriptor not found.": "O descritor da carteira não foi encontrado.",
diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json
index 7f4ef2f..4b47d0d 100644
--- a/i18n/translations/ru-RU.json
+++ b/i18n/translations/ru-RU.json
@@ -13,9 +13,7 @@
"12 words": "12 слов",
"24 words": "24 слова",
"About": "О Программе",
- "Accept assumption?": "Принять предположение?",
"Account": "Учетная запись",
- "Account #0 would be assumed": "Будет принят счет №0",
"Account Index": "Индекс счета",
"Add or change wallet passphrase?": "Добавить или изменить пароль кошелька?",
"Additional entropy from camera required for %s": "Требуется дополнительная энтропия от камеры для %s",
@@ -40,6 +38,7 @@
"Capture cancelled": "Захват отменен",
"Change": "Сдача",
"Change theme and reboot?": "Сменить тему и перезагрузить?",
+ "Change wallet?": "Сменить кошелёк?",
"Change:": "Сдача:",
"Changes will last until shutdown.": "Изменения будут храниться до выключения.",
"Check SD Card": "Проверить SD Карту",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "Мнемонический XOR",
"Mnemonic and passphrase will be kept.": "Мнемоника и парольная фраза будут сохранены.",
"Modified:": "Изменено:",
- "Native Segwit - 84 would be assumed": "Native Segwit - 84 будет принято",
"Network": "Сеть",
"New Mnemonic": "Новая Мнемоника",
"New firmware detected.": "Обнаружена новая прошивка.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Дождитесь Захвата",
"Wallet": "Кошелек",
"Wallet Descriptor": "Дескриптор Кошелька",
+ "Wallet mismatch:": "Кошелёк не совпадает:",
"Wallet output descriptor": "Выходной дескриптор кошелька",
"Wallet output descriptor loaded!": "Выходной дескриптор кошелька загружен!",
"Wallet output descriptor not found.": "Выходной дескриптор кошелька не найден.",
diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json
index 7360202..97747a7 100644
--- a/i18n/translations/tr-TR.json
+++ b/i18n/translations/tr-TR.json
@@ -13,9 +13,7 @@
"12 words": "12 kelime",
"24 words": "24 kelime",
"About": "Hakkında",
- "Accept assumption?": "Varsayım kabul edilsin mi?",
"Account": "Hesap",
- "Account #0 would be assumed": "#0 numaralı hesap varsayılacaktır",
"Account Index": "Hesap Endeksi",
"Add or change wallet passphrase?": "Cüzdan parolası eklensin mi veya değiştirilsin mi?",
"Additional entropy from camera required for %s": "%s için kameradan gelen ek entropi gerekli",
@@ -40,6 +38,7 @@
"Capture cancelled": "Yakalama iptal edildi",
"Change": "Para Üstü",
"Change theme and reboot?": "Temayı değiştir ve yeniden başlat?",
+ "Change wallet?": "Cüzdan değiştir?",
"Change:": "Para Üstü:",
"Changes will last until shutdown.": "Değişiklikler kapanışa kadar devam edecek.",
"Check SD Card": "SD Kartı Kontrol Et",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "Anımsatıcı XOR",
"Mnemonic and passphrase will be kept.": "Mnemonik ve parola tutulacaktır.",
"Modified:": "Değiştirildi:",
- "Native Segwit - 84 would be assumed": "Yerel Segwit - 84 varsayılacaktır",
"Network": "Ağ",
"New Mnemonic": "Yeni Mnemonic",
"New firmware detected.": "Yeni donanım yazılımı tespit edildi.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Yakalamanın tamamlanmasını bekleyin",
"Wallet": "Cüzdan",
"Wallet Descriptor": "Cüzdan Tanımlayıcısı",
+ "Wallet mismatch:": "Cüzdan uyuşmazlığı:",
"Wallet output descriptor": "Cüzdan çıktı tanımlayıcısı",
"Wallet output descriptor loaded!": "Cüzdan çıktı tanımlayıcısı yüklendi!",
"Wallet output descriptor not found.": "Cüzdan çıktı tanımlayıcısı bulunamadı.",
diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json
index 35bb6f2..d901f57 100644
--- a/i18n/translations/vi-VN.json
+++ b/i18n/translations/vi-VN.json
@@ -13,9 +13,7 @@
"12 words": "12 từ",
"24 words": "24 từ",
"About": "Về chúng tôi",
- "Accept assumption?": "Chấp nhận giả định?",
"Account": "Tài khoản",
- "Account #0 would be assumed": "Tài khoản #0 sẽ được giả định",
"Account Index": "Chỉ mục tài khoản",
"Add or change wallet passphrase?": "Thêm hoặc thay đổi cụm mật khẩu ví?",
"Additional entropy from camera required for %s": "Entropy bổ sung từ máy ảnh cần thiết cho %s",
@@ -40,6 +38,7 @@
"Capture cancelled": "Hủy chụp hình",
"Change": "Thay đổi",
"Change theme and reboot?": "Thay đổi giao diện và khởi động lại?",
+ "Change wallet?": "Đổi ví?",
"Change:": "Thay đổi:",
"Changes will last until shutdown.": "Thay đổi sẽ kéo dài cho đến khi tắt máy.",
"Check SD Card": "Kiểm tra thẻ SD",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "Mnemonic XOR",
"Mnemonic and passphrase will be kept.": "Từ gợi nhớ và cụm mật khẩu sẽ được lưu giữ.",
"Modified:": "Đã sửa đổi:",
- "Native Segwit - 84 would be assumed": "Native Segwit - 84 sẽ được giả định",
"Network": "Mạng lưới",
"New Mnemonic": "Mnemonic mới",
"New firmware detected.": "Phát hiện phần sụn mới.",
@@ -341,6 +339,7 @@
"Wait for the capture": "Chờ bắt",
"Wallet": "Ví",
"Wallet Descriptor": "Trình mô tả ví",
+ "Wallet mismatch:": "Ví không khớp:",
"Wallet output descriptor": "Ví đầu ra mô tả",
"Wallet output descriptor loaded!": "Đã tải bộ mô tả đầu ra của ví!",
"Wallet output descriptor not found.": "Không tìm thấy bộ mô tả đầu ra ví.",
diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json
index b803a2d..e6deb37 100644
--- a/i18n/translations/zh-CN.json
+++ b/i18n/translations/zh-CN.json
@@ -13,9 +13,7 @@
"12 words": "12 单词",
"24 words": "24 单词",
"About": "关于",
- "Accept assumption?": "接受假设?",
"Account": "账户",
- "Account #0 would be assumed": "将假定为账户 #0",
"Account Index": "账户索引",
"Add or change wallet passphrase?": "添加或更改钱包密码?",
"Additional entropy from camera required for %s": "%s需要摄像头的额外熵",
@@ -40,6 +38,7 @@
"Capture cancelled": "截取已取消",
"Change": "找零",
"Change theme and reboot?": "更改主题并重新启动?",
+ "Change wallet?": "更换钱包?",
"Change:": "找零:",
"Changes will last until shutdown.": "更改将在关机前保持.",
"Check SD Card": "检查 SD 卡",
@@ -176,7 +175,6 @@
"Mnemonic XOR": "助记符异或",
"Mnemonic and passphrase will be kept.": "助记词和密码将被保留.",
"Modified:": "修改时间:",
- "Native Segwit - 84 would be assumed": "假定为原生 Segwit - 84",
"Network": "网络",
"New Mnemonic": "新助记词",
"New firmware detected.": "检测到新固件.",
@@ -341,6 +339,7 @@
"Wait for the capture": "等待截取",
"Wallet": "钱包",
"Wallet Descriptor": "钱包描述",
+ "Wallet mismatch:": "钱包不匹配:",
"Wallet output descriptor": "钱包输出描述符",
"Wallet output descriptor loaded!": "钱包输出描述符加载重复!",
"Wallet output descriptor not found.": "未找到钱包输出描述符.",
diff --git a/src/krux/key.py b/src/krux/key.py
index 7312794..7559f4e 100644
--- a/src/krux/key.py
+++ b/src/krux/key.py
@@ -131,6 +131,13 @@ POLICY_TYPE_IDS = {
NAME_MINISCRIPT: TYPE_MINISCRIPT,
}
+# Reverse mapping: policy type ID to name
+POLICY_TYPE_NAMES_MAP = {
+ TYPE_SINGLESIG: NAME_SINGLE_SIG,
+ TYPE_MULTISIG: NAME_MULTISIG,
+ TYPE_MINISCRIPT: NAME_MINISCRIPT,
+}
+
FINGERPRINT_SYMBOL = "⊚"
DERIVATION_PATH_SYMBOL = "↳"
@@ -345,3 +352,15 @@ class Key:
candidates.append(last_word)
return candidates
+
+
+def get_policy_type_name(policy_type):
+ """Returns human-readable name for a policy type constant"""
+ return POLICY_TYPE_NAMES_MAP.get(policy_type, "Unknown")
+
+
+def get_network_name(network):
+ """Returns human-readable name for a network object (Mainnet or Testnet)"""
+ if not network:
+ return None
+ return "Mainnet" if network == NETWORKS["main"] else "Testnet"
diff --git a/src/krux/pages/home_pages/wallet_descriptor.py b/src/krux/pages/home_pages/wallet_descriptor.py
index 93fc90c..344e82a 100644
--- a/src/krux/pages/home_pages/wallet_descriptor.py
+++ b/src/krux/pages/home_pages/wallet_descriptor.py
@@ -122,10 +122,11 @@ class WalletDescriptor(Page):
return MENU_CONTINUE
- def _load_wallet(self):
- """Load a wallet output descriptor from the camera or SD card"""
-
+ def _load_wallet_data(self):
+ """Loads and decrypts wallet data from camera or SD card"""
persisted = False
+ wallet_data = None
+
load_method = self.load_method()
if load_method == LOAD_FROM_CAMERA:
from ..qr_capture import QRCodeCapture
@@ -150,15 +151,16 @@ class WalletDescriptor(Page):
except OSError:
pass
else: # Cancel
- return MENU_CONTINUE
+ return None, None, False
self.ctx.display.clear()
self.ctx.display.draw_centered_text(t("Processing…"))
if wallet_data is None:
# Camera or SD card loading failed!
self.flash_error(t("Failed to load"))
- return MENU_CONTINUE
+ return None, None, False
+ # Decrypt if needed
from ..encryption_ui import decrypt_kef
try:
@@ -169,31 +171,167 @@ class WalletDescriptor(Page):
wallet_data = wallet_data.decode()
except:
self.flash_error(t("Failed to load"))
- return MENU_CONTINUE
+ return None, None, False
except KeyError:
self.flash_error(t("Failed to decrypt"))
- return MENU_CONTINUE
+ return None, None, False
except ValueError:
# ValueError=not KEF or declined to decrypt
pass
- from ...wallet import Wallet, AssumptionWarning
+ return wallet_data, qr_format, persisted
+
+ def _build_mismatch_warning(
+ self,
+ current_policy_type,
+ required_policy_type,
+ current_network,
+ required_network,
+ current_script_type=None,
+ required_script_type=None,
+ ):
+ """Builds warning text for policy/network/script type mismatch"""
+ from ...key import get_policy_type_name, get_network_name
+
+ offset_y = DEFAULT_PADDING + 2 * FONT_HEIGHT
+
+ # Check if policy changed
+ if current_policy_type != required_policy_type:
+ self.ctx.display.draw_hcentered_text(
+ "{}:".format(t("Policy Type")), offset_y, theme.highlight_color
+ )
+ offset_y += FONT_HEIGHT
+ offset_y += (
+ self.ctx.display.draw_hcentered_text(
+ "{} > {}".format(
+ get_policy_type_name(current_policy_type),
+ get_policy_type_name(required_policy_type),
+ ),
+ offset_y,
+ )
+ * FONT_HEIGHT
+ )
+ offset_y += 2 * FONT_HEIGHT
+
+ # Check if script type changed
+ if (
+ required_script_type
+ and current_script_type
+ and current_script_type != required_script_type
+ ):
+ self.ctx.display.draw_hcentered_text(
+ "{}:".format(t("Script Type")), offset_y, theme.highlight_color
+ )
+ offset_y += FONT_HEIGHT
+ offset_y += (
+ self.ctx.display.draw_hcentered_text(
+ "{} > {}".format(
+ current_script_type.upper(),
+ required_script_type.upper(),
+ ),
+ offset_y,
+ )
+ * FONT_HEIGHT
+ )
+ offset_y += 2 * FONT_HEIGHT
+
+ # Check if network changed
+ if required_network and current_network != required_network:
+ self.ctx.display.draw_hcentered_text(
+ "{}:".format(t("Network")), offset_y, theme.highlight_color
+ )
+ offset_y += FONT_HEIGHT
+ offset_y += (
+ self.ctx.display.draw_hcentered_text(
+ "{} > {}".format(
+ get_network_name(current_network),
+ get_network_name(required_network),
+ ),
+ offset_y,
+ )
+ * FONT_HEIGHT
+ )
+
+ def _handle_policy_mismatch(self, exception, wallet_data, qr_format, persisted):
+ """Handles policy/network mismatch by prompting user and re-deriving key"""
+ from ...wallet import Wallet
+ from ...key import Key
+
+ required_policy_type = exception.args[0]
+ required_script_type = exception.args[1]
+ current_policy_type = exception.args[2]
+ required_network = exception.args[3] if len(exception.args) > 3 else None
+ current_network = exception.args[4] if len(exception.args) > 4 else None
+ current_script_type = self.ctx.wallet.key.script_type
+
+ self.ctx.display.clear()
+ self.ctx.display.draw_hcentered_text(
+ t("Wallet mismatch:"), DEFAULT_PADDING, theme.error_color
+ )
+
+ self._build_mismatch_warning(
+ current_policy_type,
+ required_policy_type,
+ current_network,
+ required_network,
+ current_script_type,
+ required_script_type,
+ )
+
+ if not self.prompt(t("Change wallet?"), BOTTOM_PROMPT_LINE):
+ return None, None
+
+ # Re-derive key with correct policy type, script type, and network
+ updated_key = Key(
+ self.ctx.wallet.key.mnemonic,
+ required_policy_type,
+ network=(
+ required_network if required_network else self.ctx.wallet.key.network
+ ),
+ passphrase=self.ctx.wallet.key.passphrase,
+ account_index=self.ctx.wallet.key.account_index,
+ script_type=required_script_type,
+ )
+
+ wallet = Wallet(updated_key)
+ wallet.persisted = persisted
+ try:
+ wallet.load(wallet_data, qr_format)
+ self.ctx.wallet.key = updated_key
+ return wallet, None
+ except Exception as e:
+ return wallet, e
+
+ def _attempt_wallet_load(self, wallet_data, qr_format, persisted):
+ """Attempts to load wallet with exception handling"""
+ from ...wallet import Wallet, PolicyMismatchWarning
wallet = Wallet(self.ctx.wallet.key)
wallet.persisted = persisted
wallet_load_exception = None
+
try:
wallet.load(wallet_data, qr_format)
- except AssumptionWarning as e:
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(e.args[0], theme.error_color)
- if self.prompt(t("Accept assumption?"), BOTTOM_PROMPT_LINE):
- try:
- wallet.load(wallet_data, qr_format, allow_assumption=e.args[1])
- except Exception as e_again:
- wallet_load_exception = e_again
+ except PolicyMismatchWarning as e:
+ return self._handle_policy_mismatch(e, wallet_data, qr_format, persisted)
except Exception as e:
wallet_load_exception = e
+
+ return wallet, wallet_load_exception
+
+ def _load_wallet(self):
+ """Load a wallet output descriptor from the camera or SD card"""
+ # Load wallet data
+ wallet_data, qr_format, persisted = self._load_wallet_data()
+ if wallet_data is None:
+ return MENU_CONTINUE
+
+ # Attempt to load the wallet with exception handling
+ wallet, wallet_load_exception = self._attempt_wallet_load(
+ wallet_data, qr_format, persisted
+ )
+
+ # Handle loading errors
if wallet_load_exception:
self.ctx.display.clear()
self.ctx.display.draw_centered_text(
@@ -201,21 +339,27 @@ class WalletDescriptor(Page):
theme.error_color,
)
self.ctx.input.wait_for_button()
+ return MENU_CONTINUE
- if wallet.is_loaded():
- if not wallet.has_change_addr():
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(
- t("Could not determine change address."), theme.error_color
- )
- if not self.prompt(t("Proceed anyway?"), BOTTOM_PROMPT_LINE):
- return MENU_CONTINUE
+ # Check if wallet was successfully loaded
+ if not wallet or not wallet.is_loaded():
+ return MENU_CONTINUE
+ # Warn about missing change address if needed
+ if not wallet.has_change_addr():
self.ctx.display.clear()
- self.display_loading_wallet(wallet)
- if self.prompt(t("Load?"), BOTTOM_PROMPT_LINE):
- self.ctx.wallet = wallet
- self.flash_text(t("Wallet output descriptor loaded!"))
+ self.ctx.display.draw_centered_text(
+ t("Could not determine change address."), theme.error_color
+ )
+ if not self.prompt(t("Proceed anyway?"), BOTTOM_PROMPT_LINE):
+ return MENU_CONTINUE
+
+ # Display wallet and confirm loading
+ self.ctx.display.clear()
+ self.display_loading_wallet(wallet)
+ if self.prompt(t("Load?"), BOTTOM_PROMPT_LINE):
+ self.ctx.wallet = wallet
+ self.flash_text(t("Wallet output descriptor loaded!"))
return MENU_CONTINUE
diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py
index fffc9af..82562ad 100644
--- a/src/krux/translations/__init__.py
+++ b/src/krux/translations/__init__.py
@@ -47,9 +47,7 @@ ref_array = [
2739590230,
1310058127,
1949634023,
- 4287534872,
2995482424,
- 2415648848,
1043817877,
3439746594,
4121028614,
@@ -74,6 +72,7 @@ ref_array = [
3138204438,
1207696150,
1583186953,
+ 2701393562,
3768416405,
388908871,
3442025874,
@@ -210,7 +209,6 @@ ref_array = [
1584070924,
1443208255,
1237332019,
- 4265479636,
2939797024,
73574491,
3634967631,
@@ -375,6 +373,7 @@ ref_array = [
2504354847,
2076481321,
2297028319,
+ 3409743444,
4232654916,
2587172867,
2499782468,
diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py
index de8ed8f..61c0455 100644
--- a/src/krux/translations/de.py
+++ b/src/krux/translations/de.py
@@ -35,9 +35,7 @@ translation_array = [
"12 Wörter",
"24 Wörter",
"Über",
- "Annahme akzeptieren?",
"Konto",
- "Konto #0 würde angenommen",
"Kontoindex",
"Wallet-Passphrase hinzufügen oder ändern?",
"Zusätzliche Entropie von der Kamera erforderlich für %s",
@@ -62,6 +60,7 @@ translation_array = [
"Aufnahme abgebrochen",
"Change Adresse",
"Thema ändern und neu starten?",
+ "Geldbörse ändern?",
"Change:",
"Änderungen bleiben bis zum Herunterfahren bestehen.",
"Prüfe SD-Karte",
@@ -198,7 +197,6 @@ translation_array = [
"Mnemonisches XOR",
"Mnemotechnik und Passphrase werden beibehalten.",
"Geändert:",
- "Native Segwit - 84 würde angenommen",
"Netzwerk",
"Neue Mnemonic",
"Neue Firmware erkannt.",
@@ -363,6 +361,7 @@ translation_array = [
"Warte auf die Erfassung",
"Wallet",
"Wallet-Deskriptor",
+ "Geldbörse passt nicht:",
"Wallet Ausgabedeskriptor",
"Wallet Ausgabedeskriptor geladen!",
"Wallet Ausgabedeskriptor nicht gefunden.",
diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py
index 99c7e04..865eb4a 100644
--- a/src/krux/translations/es.py
+++ b/src/krux/translations/es.py
@@ -35,9 +35,7 @@ translation_array = [
"12 palabras",
"24 palabras",
"Nosotros",
- "¿Aceptar la suposición?",
"Cuenta",
- "Se supondría que la cuenta #0",
"Índice de la cuenta",
"¿Añadir o cambiar passphrase de la cartera?",
"Se requiere entropía adicional de la cámara para %s",
@@ -62,6 +60,7 @@ translation_array = [
"Captura cancelada",
"Cambio",
"¿Cambiar de tema y reiniciar?",
+ "¿Cambiar cartera?",
"Cambio:",
"Los cambios durarán hasta que el dispositivo se apague.",
"Revisar Tarjeta SD",
@@ -198,7 +197,6 @@ translation_array = [
"XOR mnemónico",
"Mnemónico y passphrase se mantendrán.",
"Modificado:",
- "Segwit nativo - 84 se supondría",
"Red",
"Nuevo Mnemónico",
"Nuevo firmware detectado.",
@@ -363,6 +361,7 @@ translation_array = [
"Espera la captura",
"Cartera",
"Descriptor de Cartera",
+ "Cartera no coincide:",
"Descriptor de salida de cartera",
"¡Se ha cargado el descriptor de salida de la cartera!",
"No se encontró el descriptor de salida de la cartera.",
diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py
index 171e09f..c5f4b32 100644
--- a/src/krux/translations/fr.py
+++ b/src/krux/translations/fr.py
@@ -35,9 +35,7 @@ translation_array = [
"12 mots",
"24 mots",
"À propos",
- "Accepter supposition\u2009?",
"Compte",
- "Le compte n °0 serait supposé",
"Index du compte",
"Ajoutez ou modifiez la phrase secrète\u2009?",
"Entropie supplémentaire de la caméra requise pour %s",
@@ -62,6 +60,7 @@ translation_array = [
"Capture annulée",
"Monnaie",
"Changer de thème et redémarrer\u2009?",
+ "Changer portefeuille?",
"La monnaie\u2009:",
"Les modifications dureront jusqu'à l'arrêt.",
"Vérifiez la carte SD",
@@ -198,7 +197,6 @@ translation_array = [
"Mnémonique XOR",
"Mnémonique et phrase secrète seront conservés.",
"Modifié\u2009:",
- "Native Segwit - 84 serait supposé",
"Réseau",
"Nouveau Mnémonique",
"Nouveau micrologiciel détecté.",
@@ -363,6 +361,7 @@ translation_array = [
"Attendez la capture",
"Portefeuille",
"Descripteur de Portefeuille",
+ "Portefeuille différent:",
"Descripteur de sortie du portefeuille",
"Descripteur de sortie du portefeuille chargé\u2009!",
"Descripteur de sortie du portefeuille introuvable.",
diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py
index f6e7904..c002e68 100644
--- a/src/krux/translations/ja.py
+++ b/src/krux/translations/ja.py
@@ -35,9 +35,7 @@ translation_array = [
"12の単語",
"24の単語",
"概要",
- "仮定を受け入れますか?",
"アカウント",
- "アカウント#0は仮定されます",
"アカウントインデックス",
"ウォレットのパスフレーズを追加または変更しますか?",
"%sにはカメラからの追加エントロピーが必要です",
@@ -62,6 +60,7 @@ translation_array = [
"キャプチャがキャンセルされました",
"お釣り",
"テーマの変更と再起動しますか?",
+ "ウォレット変更?",
"お釣り:",
"変更はシャットダウンするまで持続します.",
"SDカードを確認する",
@@ -198,7 +197,6 @@ translation_array = [
"ニーモニックXOR",
"Mnemonicとパスフレーズは保持されます.",
"修正されました:",
- "ネイティブSegwit - 84が仮定されます",
"ネットワーク",
"新しい Mnemonic",
"新しいファームウェアが検出されました.",
@@ -363,6 +361,7 @@ translation_array = [
"キャプチャを待ってください",
"ワレット",
"ウォレットディスクリプター",
+ "ウォレット不一致:",
"ウォレット出力ディスクリプター",
"ウォレット出力ディスクリプターがロードされました!",
"ウォレット出力ディスクリプターが見つかりません.",
diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py
index b4f44d7..4bb8dae 100644
--- a/src/krux/translations/ko.py
+++ b/src/krux/translations/ko.py
@@ -35,9 +35,7 @@ translation_array = [
"12단어",
"24단어",
"기본 정보",
- "가정을 수락하시겠습니까?",
"계정",
- "계정 #0이 가정됩니다",
"계정 인덱스",
"패스프레이즈를 추가하거나 변경하시겠습니까?",
"%s 에 필요한 카메라의 추가 엔트로피",
@@ -62,6 +60,7 @@ translation_array = [
"캡처 취소됨",
"잔돈",
"테마를 변경하고 재부팅하시겠습니까?",
+ "지갑 변경?",
"잔돈:",
"변경 사항은 종료될때까지 유지됩니다.",
"SD카드 확인",
@@ -198,7 +197,6 @@ translation_array = [
"니모닉 XOR",
"니모닉과 암호는 유지됩니다.",
"수정되었습니다:",
- "네이티브 세그윗 - BIP84를 적용합니다",
"네트워크",
"새로운 니모닉",
"새로운 펌웨어가 감지되었습니다.",
@@ -363,6 +361,7 @@ translation_array = [
"캡처될때까지 기다리십시오",
"지갑 설정",
"지갑 디스크립터",
+ "지갑 불일치:",
"지갑 출력 디스크립터",
"지갑 출력 디스크립터가 로드되었습니다!",
"지갑 출력 디스크립터를 찾을 수 없습니다.",
diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py
index 2869e81..968f406 100644
--- a/src/krux/translations/nl.py
+++ b/src/krux/translations/nl.py
@@ -35,9 +35,7 @@ translation_array = [
"12 woorden",
"24 woorden",
"Over",
- "Aanname accepteren?",
"Account",
- "Account #0 zou worden aangenomen",
"Accountindex",
"Wachtwoordzin voor portemonnee toevoegen of wijzigen?",
"Extra entropie van camera vereist voor %s",
@@ -62,6 +60,7 @@ translation_array = [
"Opname geannuleerd",
"Change",
"Thema veranderen en opnieuw opstarten?",
+ "Portemonnee wijzigen?",
"Wisselgeld:",
"Wijzigingen blijven van kracht tot afsluiten.",
"Controleer SD kaart",
@@ -198,7 +197,6 @@ translation_array = [
"Mnemonic XOR",
"Geheugensteun en wachtwoord worden bewaard.",
"Aangepast:",
- "Native Segwit - 84 zal worden gebruikt",
"Netwerk",
"Geheugensteun aanmaken",
"Nieuwe firmware gevonden.",
@@ -363,6 +361,7 @@ translation_array = [
"Wacht op opname",
"Portemonnee",
"Descriptor",
+ "Portemonnee onjuist:",
"Portemonnee descriptor",
"Portemonnee descriptor geladen!",
"Portemonnee descriptor niet gevonden.",
diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py
index ed6f183..d26a531 100644
--- a/src/krux/translations/pt.py
+++ b/src/krux/translations/pt.py
@@ -35,9 +35,7 @@ translation_array = [
"12 palavras",
"24 palavras",
"Sobre",
- "Aceitar suposições?",
"Conta",
- "A conta #0 seria assumida",
"Índice da Conta",
"Adicionar ou alterar a senha da carteira?",
"Entropia adicional da câmera é necessária para %s",
@@ -62,6 +60,7 @@ translation_array = [
"Captura cancelada",
"Troco",
"Mudar o tema e reiniciar?",
+ "Mudar carteira?",
"Troco:",
"As alterações só durarão até o desligamento.",
"Explorar o cartão SD",
@@ -198,7 +197,6 @@ translation_array = [
"XOR Mnemônico",
"Mnemônico e senha serão mantidos.",
"Alterado:",
- "Segwit nativo - 84 seria assumido",
"Rede",
"Novo Mnemônico",
"Novo firmware detectado.",
@@ -363,6 +361,7 @@ translation_array = [
"Aguarde a captura",
"Carteira",
"Descritor da Carteira",
+ "Carteira diferente:",
"Descritor da carteira",
"Descritor da carteira carregado!",
"O descritor da carteira não foi encontrado.",
diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py
index 5440ebb..1327eb7 100644
--- a/src/krux/translations/ru.py
+++ b/src/krux/translations/ru.py
@@ -35,9 +35,7 @@ translation_array = [
"12 слов",
"24 слова",
"О Программе",
- "Принять предположение?",
"Учетная запись",
- "Будет принят счет №0",
"Индекс счета",
"Добавить или изменить пароль кошелька?",
"Требуется дополнительная энтропия от камеры для %s",
@@ -62,6 +60,7 @@ translation_array = [
"Захват отменен",
"Сдача",
"Сменить тему и перезагрузить?",
+ "Сменить кошелёк?",
"Сдача:",
"Изменения будут храниться до выключения.",
"Проверить SD Карту",
@@ -198,7 +197,6 @@ translation_array = [
"Мнемонический XOR",
"Мнемоника и парольная фраза будут сохранены.",
"Изменено:",
- "Native Segwit - 84 будет принято",
"Сеть",
"Новая Мнемоника",
"Обнаружена новая прошивка.",
@@ -363,6 +361,7 @@ translation_array = [
"Дождитесь Захвата",
"Кошелек",
"Дескриптор Кошелька",
+ "Кошелёк не совпадает:",
"Выходной дескриптор кошелька",
"Выходной дескриптор кошелька загружен!",
"Выходной дескриптор кошелька не найден.",
diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py
index 37b1876..fd1cc29 100644
--- a/src/krux/translations/tr.py
+++ b/src/krux/translations/tr.py
@@ -35,9 +35,7 @@ translation_array = [
"12 kelime",
"24 kelime",
"Hakkında",
- "Varsayım kabul edilsin mi?",
"Hesap",
- "#0 numaralı hesap varsayılacaktır",
"Hesap Endeksi",
"Cüzdan parolası eklensin mi veya değiştirilsin mi?",
"%s için kameradan gelen ek entropi gerekli",
@@ -62,6 +60,7 @@ translation_array = [
"Yakalama iptal edildi",
"Para Üstü",
"Temayı değiştir ve yeniden başlat?",
+ "Cüzdan değiştir?",
"Para Üstü:",
"Değişiklikler kapanışa kadar devam edecek.",
"SD Kartı Kontrol Et",
@@ -198,7 +197,6 @@ translation_array = [
"Anımsatıcı XOR",
"Mnemonik ve parola tutulacaktır.",
"Değiştirildi:",
- "Yerel Segwit - 84 varsayılacaktır",
"Ağ",
"Yeni Mnemonic",
"Yeni donanım yazılımı tespit edildi.",
@@ -363,6 +361,7 @@ translation_array = [
"Yakalamanın tamamlanmasını bekleyin",
"Cüzdan",
"Cüzdan Tanımlayıcısı",
+ "Cüzdan uyuşmazlığı:",
"Cüzdan çıktı tanımlayıcısı",
"Cüzdan çıktı tanımlayıcısı yüklendi!",
"Cüzdan çıktı tanımlayıcısı bulunamadı.",
diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py
index 11d7f42..28e26bf 100644
--- a/src/krux/translations/vi.py
+++ b/src/krux/translations/vi.py
@@ -35,9 +35,7 @@ translation_array = [
"12 từ",
"24 từ",
"Về chúng tôi",
- "Chấp nhận giả định?",
"Tài khoản",
- "Tài khoản #0 sẽ được giả định",
"Chỉ mục tài khoản",
"Thêm hoặc thay đổi cụm mật khẩu ví?",
"Entropy bổ sung từ máy ảnh cần thiết cho %s",
@@ -62,6 +60,7 @@ translation_array = [
"Hủy chụp hình",
"Thay đổi",
"Thay đổi giao diện và khởi động lại?",
+ "Đổi ví?",
"Thay đổi:",
"Thay đổi sẽ kéo dài cho đến khi tắt máy.",
"Kiểm tra thẻ SD",
@@ -198,7 +197,6 @@ translation_array = [
"Mnemonic XOR",
"Từ gợi nhớ và cụm mật khẩu sẽ được lưu giữ.",
"Đã sửa đổi:",
- "Native Segwit - 84 sẽ được giả định",
"Mạng lưới",
"Mnemonic mới",
"Phát hiện phần sụn mới.",
@@ -363,6 +361,7 @@ translation_array = [
"Chờ bắt",
"Ví",
"Trình mô tả ví",
+ "Ví không khớp:",
"Ví đầu ra mô tả",
"Đã tải bộ mô tả đầu ra của ví!",
"Không tìm thấy bộ mô tả đầu ra ví.",
diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py
index bf187d3..24f26a3 100644
--- a/src/krux/translations/zh.py
+++ b/src/krux/translations/zh.py
@@ -35,9 +35,7 @@ translation_array = [
"12 单词",
"24 单词",
"关于",
- "接受假设?",
"账户",
- "将假定为账户 #0",
"账户索引",
"添加或更改钱包密码?",
"%s需要摄像头的额外熵",
@@ -62,6 +60,7 @@ translation_array = [
"截取已取消",
"找零",
"更改主题并重新启动?",
+ "更换钱包?",
"找零:",
"更改将在关机前保持.",
"检查 SD 卡",
@@ -198,7 +197,6 @@ translation_array = [
"助记符异或",
"助记词和密码将被保留.",
"修改时间:",
- "假定为原生 Segwit - 84",
"网络",
"新助记词",
"检测到新固件.",
@@ -363,6 +361,7 @@ translation_array = [
"等待截取",
"钱包",
"钱包描述",
+ "钱包不匹配:",
"钱包输出描述符",
"钱包输出描述符加载重复!",
"未找到钱包输出描述符.",
diff --git a/src/krux/wallet.py b/src/krux/wallet.py
index 11edfc9..b3948e1 100644
--- a/src/krux/wallet.py
+++ b/src/krux/wallet.py
@@ -22,7 +22,6 @@
from embit.descriptor.descriptor import Descriptor
from embit.descriptor.arguments import Key
from embit.networks import NETWORKS
-from embit.bip32 import HARDENED_INDEX
from .krux_settings import t
from .qr import FORMAT_BBQR, FORMAT_NONE
from .key import (
@@ -33,16 +32,14 @@ from .key import (
P2WPKH,
P2WSH,
P2TR,
- SINGLESIG_SCRIPT_PURPOSE,
- MULTISIG_SCRIPT_PURPOSE,
TYPE_SINGLESIG,
TYPE_MULTISIG,
TYPE_MINISCRIPT,
)
-class AssumptionWarning(Exception):
- """An exception for assumptions that require user acceptance"""
+class PolicyMismatchWarning(Exception):
+ """An exception for wallet policy type mismatches that require user acceptance"""
class Wallet:
@@ -133,8 +130,47 @@ class Wallet:
"""Returns a boolean indicating whether or not this wallet has been loaded"""
return self.wallet_data is not None
- def _validate_descriptor(self, descriptor, descriptor_xpubs):
- """Validates the descriptor against the current key and policy type"""
+ def _determine_descriptor_policy(self, descriptor):
+ """Returns required policy type and script type from descriptor"""
+ descriptor_is_multisig = descriptor.is_basic_multisig
+ descriptor_is_miniscript = not descriptor_is_multisig and (
+ descriptor.miniscript is not None or descriptor.taptree
+ )
+ descriptor_is_singlesig = (
+ not descriptor_is_multisig
+ and not descriptor_is_miniscript
+ and descriptor.key is not None
+ )
+
+ if descriptor_is_multisig:
+ return TYPE_MULTISIG, None
+ if descriptor_is_miniscript:
+ script_type = P2TR if descriptor.taptree else P2WSH
+ return TYPE_MINISCRIPT, script_type
+ if descriptor_is_singlesig or (
+ not descriptor.key and len(descriptor.keys) == 1
+ ):
+ script_type = descriptor.scriptpubkey_type()
+ # Check for nested segwit (sh(wpkh(...)))
+ if script_type == "p2sh" and hasattr(descriptor, "witness_script"):
+ if descriptor.witness_script:
+ # It's sh(wpkh(...)) = P2SH-P2WPKH
+ script_type = P2SH_P2WPKH
+ return TYPE_SINGLESIG, script_type
+
+ raise ValueError("Unable to determine descriptor policy type")
+
+ def _determine_descriptor_network(self, descriptor):
+ """Returns the network from descriptor's xpub version"""
+ version = descriptor.keys[0].key.version
+ for em_network in ("main", "test"):
+ for em_vertype in ("xpub", "ypub", "zpub", "Ypub", "Zpub"):
+ if version == NETWORKS[em_network][em_vertype]:
+ return NETWORKS[em_network]
+ return None
+
+ def _validate_xpub_match(self, descriptor, descriptor_xpubs):
+ """Validates that key's xpub matches the descriptor"""
if self.is_multisig():
if not descriptor.is_basic_multisig:
raise ValueError("not multisig")
@@ -150,15 +186,44 @@ class Wallet:
if self.key.xpub() not in descriptor_xpubs:
raise ValueError("xpub not a miniscript cosigner")
else:
- if not descriptor.key:
- if len(descriptor.keys) > 1:
- raise ValueError("not single-sig")
+ if not descriptor.key and len(descriptor.keys) > 1:
+ raise ValueError("not single-sig")
if self.key.xpub() != descriptor_xpubs[0]:
raise ValueError("xpub does not match")
- def load(self, wallet_data, qr_format, allow_assumption=None):
+ def _validate_descriptor(self, descriptor, descriptor_xpubs):
+ """Validates the descriptor against the current key and policy type"""
+ # Determine required policy and script type from descriptor
+ required_policy_type, required_script_type = self._determine_descriptor_policy(
+ descriptor
+ )
+
+ # Determine required network from descriptor
+ required_network = self._determine_descriptor_network(descriptor)
+
+ # Check for policy, script type, or network mismatch
+ policy_mismatch = self.key.policy_type != required_policy_type
+ script_type_mismatch = (
+ required_script_type is not None
+ and self.key.script_type != required_script_type
+ )
+ network_mismatch = required_network and self.key.network != required_network
+
+ if policy_mismatch or script_type_mismatch or network_mismatch:
+ raise PolicyMismatchWarning(
+ required_policy_type,
+ required_script_type,
+ self.key.policy_type,
+ required_network,
+ self.key.network,
+ )
+
+ # Validate that xpub matches descriptor requirements
+ self._validate_xpub_match(descriptor, descriptor_xpubs)
+
+ def load(self, wallet_data, qr_format):
"""Loads the wallet from the given data"""
- descriptor, label = parse_wallet(wallet_data, allow_assumption)
+ descriptor, label = parse_wallet(wallet_data)
# convert descriptor keys to 'xpub' on same network -- for comparison only
descriptor_xpubs = []
@@ -358,7 +423,7 @@ def parse_key_value_file(wallet_data):
return None, None
-def parse_wallet(wallet_data, allow_assumption=None):
+def parse_wallet(wallet_data):
"""Exhaustively tries to parse the wallet data from a known format, returning
a descriptor and label if possible.
@@ -422,31 +487,7 @@ def parse_wallet(wallet_data, allow_assumption=None):
descriptor = Descriptor.from_string(wallet_data.strip())
return descriptor, None
except:
- # If that fails, try to parse as an xpub as a last resort
- pubkey = Key.from_string(wallet_data.strip())
- if pubkey.is_extended:
- network, versiontype = version_to_network_versiontype(pubkey.key.version)
-
- xpub = pubkey.key.to_base58()
-
- fmt = None
- if pubkey.origin is None:
- # assume derivation if possible
- derivation = xpub_data_to_derivation(
- versiontype,
- network,
- pubkey.key.child_number,
- pubkey.key.depth,
- allow_assumption=allow_assumption,
- )
- if derivation:
- fmt = derivation_to_script_wrapper(derivation)
- else:
- fmt = derivation_to_script_wrapper(pubkey.origin.derivation)
- fmt = fmt.format("[" + str(pubkey.origin) + "]{}")
-
- descriptor = Descriptor.from_string(fmt.format(xpub))
- return descriptor, None
+ pass
raise ValueError("invalid wallet format")
@@ -497,96 +538,6 @@ def version_to_network_versiontype(hdkey_version):
return network, versiontype
-def xpub_data_to_derivation(versiontype, network, child, depth, allow_assumption=None):
- """returns assumed derivation list for supported slip32 bips
- based on embit.networks.NETWORKS keys for versiontype, network,
- child_number (used as account for single-sig) and depth. Depth
- is used as weak verification, it must match the expected depth.
- Where unsafe assumptions could be made, AssumptionWarning is raised
- (with warning text and assumed derivation as first two params)
- unless called with allow_assumption=assumed_derivation.
- """
-
- derivation, network_node, assumption_text = None, None, None
-
- if network == "main":
- network_node = 0 + HARDENED_INDEX
- elif network in ("test", "regtest", "signet"):
- network_node = 1 + HARDENED_INDEX
-
- if network_node and child >= HARDENED_INDEX:
- if versiontype == "xpub" and depth == 3:
- derivation = [
- SINGLESIG_SCRIPT_PURPOSE[P2WPKH] + HARDENED_INDEX,
- network_node,
- child,
- ]
- if allow_assumption != derivation:
- assumption_text = t("Native Segwit - 84 would be assumed")
- elif versiontype == "ypub" and depth == 3:
- derivation = [
- SINGLESIG_SCRIPT_PURPOSE[P2SH_P2WPKH] + HARDENED_INDEX,
- network_node,
- child,
- ]
- elif versiontype == "zpub" and depth == 3:
- derivation = [
- SINGLESIG_SCRIPT_PURPOSE[P2WPKH] + HARDENED_INDEX,
- network_node,
- child,
- ]
- elif versiontype == "Ypub" and depth == 4 and child == 1 + HARDENED_INDEX:
- derivation = [
- MULTISIG_SCRIPT_PURPOSE[P2SH_P2WSH] + HARDENED_INDEX,
- network_node,
- 0 + HARDENED_INDEX,
- child,
- ]
- if allow_assumption != derivation:
- assumption_text = t("Account #0 would be assumed")
- elif versiontype == "Zpub" and depth == 4 and child == 2 + HARDENED_INDEX:
- derivation = [
- MULTISIG_SCRIPT_PURPOSE[P2WSH] + HARDENED_INDEX,
- network_node,
- 0 + HARDENED_INDEX,
- child,
- ]
- if allow_assumption != derivation:
- assumption_text = t("Account #0 would be assumed")
-
- if assumption_text:
- raise AssumptionWarning(assumption_text, derivation)
-
- return derivation
-
-
-def derivation_to_script_wrapper(derivation):
- """returns format_str for wrapping xpub into wallet descriptor,
- supporting single-sig only for now, based on
- embit.descriptor.arguments.KeyOrigin.derivation list"""
- format_str = None
-
- if len(derivation) == 3:
- purpose = derivation[0]
- network = derivation[1]
- account = derivation[2]
-
- if (
- network in (0 + HARDENED_INDEX, 1 + HARDENED_INDEX)
- and account >= 0 + HARDENED_INDEX
- ):
- if purpose == SINGLESIG_SCRIPT_PURPOSE[P2PKH] + HARDENED_INDEX:
- format_str = "pkh({})"
- elif purpose == SINGLESIG_SCRIPT_PURPOSE[P2SH_P2WPKH] + HARDENED_INDEX:
- format_str = "sh(wpkh({}))"
- elif purpose == SINGLESIG_SCRIPT_PURPOSE[P2WPKH] + HARDENED_INDEX:
- format_str = "wpkh({})"
- elif purpose == SINGLESIG_SCRIPT_PURPOSE[P2TR] + HARDENED_INDEX:
- format_str = "tr({})"
-
- return format_str
-
-
def is_double_mnemonic(mnemonic: str):
"""Check if the mnemonic is a double mnemonic (12+12+24)"""
diff --git a/tests/pages/home_pages/test_wallet_descriptor.py b/tests/pages/home_pages/test_wallet_descriptor.py
index cda4512..b65446f 100644
--- a/tests/pages/home_pages/test_wallet_descriptor.py
+++ b/tests/pages/home_pages/test_wallet_descriptor.py
@@ -78,55 +78,7 @@ def test_wallet(mocker, m5stickv, tdata):
None,
[BUTTON_ENTER, BUTTON_ENTER],
),
- # 9 vague BlueWallet-ish p2pkh, requires allow_assumption
- (
- False,
- tdata.LEGACY1_KEY,
- tdata.VAGUE_LEGACY1_XPUB,
- None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- # 10 vague BlueWallet-ish p2pkh w/o key loaded, requires allow_assumption
- (
- False,
- None,
- tdata.VAGUE_LEGACY1_XPUB,
- None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- # 11 vague BlueWallet-ish p2sh-p2wpkh ypub
- (
- False,
- tdata.NESTEDSW1_KEY,
- tdata.VAGUE_NESTEDSW1_YPUB,
- None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- # 12 vague BlueWallet-ish p2sh-p2wpkh ypub w/o key loaded
- (
- False,
- None,
- tdata.VAGUE_NESTEDSW1_YPUB,
- None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- # 13 vague BlueWallet-ish p2wpkh zpub
- (
- False,
- tdata.NATIVESW1_KEY,
- tdata.VAGUE_NATIVESW1_ZPUB,
- None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- # 14 vague BlueWallet-ish p2wpkh zpub w/o key loaded
- (
- False,
- None,
- tdata.VAGUE_NATIVESW1_ZPUB,
- None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- # 15 Load, from SD card, good data, accept
+ # 9 Load, from SD card, good data, accept
(
False,
tdata.SINGLESIG_12_WORD_KEY,
@@ -447,3 +399,524 @@ def test_loading_miniscript_descriptors(mocker, amigo, wallet_tdata):
wallet_descriptor.display_loading_wallet.assert_called_once()
assert ctx.wallet.has_change_addr()
assert ctx.input.wait_for_button.call_count == len(case[2])
+
+
+def test_policy_mismatch_singlesig_to_multisig_accept(
+ mocker, m5stickv, tdata, wallet_tdata
+):
+ """Test: Singlesig key loading multisig descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_MULTISIG
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept policy change (singlesig -> multisig)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (wallet_tdata.UNAMBIGUOUS_MULTISIG_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with multisig policy
+ assert ctx.wallet.key.policy_type == TYPE_MULTISIG
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_policy_mismatch_singlesig_to_multisig_decline(
+ mocker, m5stickv, tdata, wallet_tdata
+):
+ """Test: Singlesig key loading multisig descriptor - user declines change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_SINGLESIG
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_PAGE, # Decline policy change
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (wallet_tdata.UNAMBIGUOUS_MULTISIG_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was NOT changed (still singlesig)
+ assert ctx.wallet.key.policy_type == TYPE_SINGLESIG
+ # Verify wallet was NOT loaded
+ assert not ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_policy_mismatch_singlesig_to_miniscript_accept(
+ mocker, m5stickv, tdata, wallet_tdata
+):
+ """Test: Singlesig key loading miniscript descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_MINISCRIPT, P2WSH
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept policy change (singlesig -> miniscript)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (wallet_tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with miniscript policy and P2WSH script type
+ assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT
+ assert ctx.wallet.key.script_type == P2WSH
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_policy_mismatch_singlesig_to_taproot_miniscript_accept(
+ mocker, m5stickv, tdata, wallet_tdata
+):
+ """Test: Singlesig key loading taproot miniscript descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_MINISCRIPT, P2TR
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept policy change (singlesig -> miniscript)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (
+ wallet_tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR,
+ FORMAT_PMOFN,
+ ),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with miniscript policy and P2TR script type
+ assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT
+ assert ctx.wallet.key.script_type == P2TR
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_policy_mismatch_multisig_to_singlesig_accept(
+ mocker, m5stickv, tdata, wallet_tdata
+):
+ """Test: Multisig key loading singlesig descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_SINGLESIG
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept policy change (multisig -> singlesig)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.MULTISIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (wallet_tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with singlesig policy
+ assert ctx.wallet.key.policy_type == TYPE_SINGLESIG
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_policy_mismatch_multisig_to_miniscript_accept(
+ mocker, m5stickv, tdata, wallet_tdata
+):
+ """Test: Multisig key loading miniscript descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_MINISCRIPT, P2WSH
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept policy change (multisig -> miniscript)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.MULTISIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (wallet_tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with miniscript policy
+ assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT
+ assert ctx.wallet.key.script_type == P2WSH
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_policy_mismatch_miniscript_to_singlesig_accept(
+ mocker, amigo, tdata, wallet_tdata
+):
+ """Test: Miniscript key loading singlesig descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_SINGLESIG
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept policy change (miniscript -> singlesig)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(wallet_tdata.MINISCRIPT_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ # Use UNAMBIGUOUS_SINGLESIG_DESCRIPTOR which has /<0;1>/* (change branch)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (wallet_tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with singlesig policy
+ assert ctx.wallet.key.policy_type == TYPE_SINGLESIG
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_network_mismatch_mainnet_to_testnet_accept(mocker, m5stickv, tdata):
+ """Test: Mainnet key loading testnet descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from embit.networks import NETWORKS
+
+ # Testnet singlesig descriptor matching TEST_12_WORD_MNEMONIC
+ # Derived from [55f8fc5d/84'/1'/0'] for testnet
+ TESTNET_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/1h/0h]tpubDCDuqu5HtBX2aD7wxvnHcj1DgFN1UVgzLkA1Ms4Va4P7TpJ3jDknkPLwWT2SqrKXNNAtJBCPcbJ8Tcpm6nLxgFapCZyhKgqwcEGv1BVpD7s/<0;1>/*)"
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept network change (mainnet -> testnet)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (TESTNET_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with testnet network
+ assert ctx.wallet.key.network == NETWORKS["test"]
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_network_mismatch_mainnet_to_testnet_decline(mocker, m5stickv, tdata):
+ """Test: Mainnet key loading testnet descriptor - user declines change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from embit.networks import NETWORKS
+
+ # Testnet singlesig descriptor matching TEST_12_WORD_MNEMONIC
+ TESTNET_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/1h/0h]tpubDCDuqu5HtBX2aD7wxvnHcj1DgFN1UVgzLkA1Ms4Va4P7TpJ3jDknkPLwWT2SqrKXNNAtJBCPcbJ8Tcpm6nLxgFapCZyhKgqwcEGv1BVpD7s/<0;1>/*)"
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_PAGE, # Decline network change
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (TESTNET_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was NOT changed (still mainnet)
+ assert ctx.wallet.key.network == NETWORKS["main"]
+ # Verify wallet was NOT loaded
+ assert not ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_script_type_mismatch_p2wpkh_to_p2tr_accept(
+ mocker, m5stickv, tdata, wallet_tdata
+):
+ """Test: P2WPKH singlesig key loading P2TR descriptor - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import P2TR, TYPE_SINGLESIG
+
+ # Taproot singlesig descriptor for same mnemonic (TEST_12_WORD_MNEMONIC)
+ TAPROOT_SINGLESIG_DESCRIPTOR = "tr([55f8fc5d/86h/0h/0h]xpub6CNGwJbVG9sQq4vfRxk6zpE4iiES3XsdVjn3SRkbHB8zDe66vhtPJUH7dCuVMmTcQ8fuozNYDYxcQJeJZE28eSD2sZYDHCYgC3Dq48kjbKy/<0;1>/*)"
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept script type change (P2WPKH -> P2TR)
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (TAPROOT_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with P2TR script type
+ assert ctx.wallet.key.policy_type == TYPE_SINGLESIG
+ assert ctx.wallet.key.script_type == P2TR
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
+
+
+def test_combined_policy_and_network_mismatch_accept(mocker, m5stickv, tdata):
+ """Test: Combined policy and network mismatch - user accepts change"""
+ from krux.pages.home_pages.wallet_descriptor import WalletDescriptor
+ from krux.wallet import Wallet
+ from krux.input import BUTTON_ENTER
+ from krux.qr import FORMAT_PMOFN
+ from krux.pages.qr_capture import QRCodeCapture
+ from krux.key import TYPE_MINISCRIPT, P2WSH
+ from embit.networks import NETWORKS
+
+ # Testnet miniscript descriptor (different policy + different network)
+ # Key matches TEST_12_WORD_MNEMONIC at path 48'/1'/0'/2' on testnet
+ # Using simple wsh(pk(...)) which is a valid miniscript construct
+ TESTNET_MINISCRIPT_DESCRIPTOR = "wsh(pk([55f8fc5d/48h/1h/0h/2h]tpubDDyrxYEe6bifecFTgj8vzsoUhoJmtVWeARR5xRun6haVnVrC2oTAYhj7Ja2KTkcnkW1mZPPuWGDxEsHMtRf8aAf4WfrqhLDN7xi9zAZMphv/<0;1>/*))"
+
+ btn_seq = [
+ BUTTON_ENTER, # Load wallet descriptor? Yes
+ BUTTON_ENTER, # Load from camera
+ BUTTON_ENTER, # Accept combined policy + network change
+ BUTTON_ENTER, # Accept wallet load
+ ]
+
+ wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY)
+ ctx = create_ctx(mocker, btn_seq, wallet)
+
+ wallet_descriptor = WalletDescriptor(ctx)
+ mocker.patch.object(
+ QRCodeCapture,
+ "qr_capture_loop",
+ new=lambda self: (TESTNET_MINISCRIPT_DESCRIPTOR, FORMAT_PMOFN),
+ )
+ mocker.patch.object(
+ wallet_descriptor,
+ "display_qr_codes",
+ new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
+ )
+ # Mock display_loading_wallet to avoid pagination button presses
+ mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None)
+ mocker.spy(wallet_descriptor, "_handle_policy_mismatch")
+
+ wallet_descriptor.wallet()
+
+ # Verify mismatch was handled
+ wallet_descriptor._handle_policy_mismatch.assert_called_once()
+ # Verify key was re-derived with miniscript policy AND testnet network
+ assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT
+ assert ctx.wallet.key.script_type == P2WSH
+ assert ctx.wallet.key.network == NETWORKS["test"]
+ # Verify wallet was loaded
+ assert ctx.wallet.is_loaded()
+ assert ctx.input.wait_for_button.call_count == len(btn_seq)
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 8f6ef51..d32a427 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -1155,8 +1155,11 @@ def test_load_singlesig(mocker, m5stickv, tdata):
assert wallet.has_change_addr()
-def test_load_singlesig_fails_with_multisig_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+def test_load_singlesig_warns_mismatch_with_multisig_descriptor(
+ mocker, m5stickv, tdata
+):
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_MULTISIG, TYPE_SINGLESIG
from krux.qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR
wallet = Wallet(tdata.SINGLESIG_KEY)
@@ -1168,24 +1171,72 @@ def test_load_singlesig_fails_with_multisig_descriptor(mocker, m5stickv, tdata):
(tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR),
]
for case in cases:
- with pytest.raises(ValueError):
+ # Should raise PolicyMismatchWarning due to policy type mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(case[0], case[1])
+ # Verify exception contains correct policy types
+ assert exc_info.value.args[0] == TYPE_MULTISIG # required_policy_type
+ assert exc_info.value.args[2] == TYPE_SINGLESIG # current_policy_type
-def test_load_singlesig_fails_with_miniscript_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+ # Note: These descriptors don't contain this key's xpub as a cosigner,
+ # so even after fixing the policy type, loading would fail with
+ # "xpub not a multisig cosigner" error
+
+
+def test_load_singlesig_warns_mismatch_with_miniscript_descriptor(
+ mocker, m5stickv, tdata
+):
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_MINISCRIPT, TYPE_SINGLESIG, P2WSH, P2TR
from krux.qr import FORMAT_NONE
wallet = Wallet(tdata.SINGLESIG_KEY)
- with pytest.raises(ValueError):
+ # Test P2WSH miniscript
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
- with pytest.raises(ValueError):
+ assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type
+ assert exc_info.value.args[1] == P2WSH # required_script_type
+ assert exc_info.value.args[2] == TYPE_SINGLESIG # current_policy_type
+
+ # Verify wallet can load with correct policy
+ miniscript_key = Key(
+ tdata.SINGLESIG_KEY.mnemonic,
+ TYPE_MINISCRIPT,
+ tdata.SINGLESIG_KEY.network,
+ tdata.SINGLESIG_KEY.passphrase,
+ script_type=P2WSH,
+ )
+ wallet_wsh = Wallet(miniscript_key)
+ wallet_wsh.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ assert wallet_wsh.is_loaded()
+
+ # Test P2TR miniscript
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type
+ assert exc_info.value.args[1] == P2TR # required_script_type
+ assert exc_info.value.args[2] == TYPE_SINGLESIG # current_policy_type
+
+ # Verify wallet can load with correct policy
+ tap_miniscript_key = Key(
+ tdata.SINGLESIG_KEY.mnemonic,
+ TYPE_MINISCRIPT,
+ tdata.SINGLESIG_KEY.network,
+ tdata.SINGLESIG_KEY.passphrase,
+ script_type=P2TR,
+ )
+ wallet_tr = Wallet(tap_miniscript_key)
+ wallet_tr.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ assert wallet_tr.is_loaded()
-def test_load_multisig_fails_with_singlesig_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+def test_load_multisig_warns_mismatch_with_singlesig_descriptor(
+ mocker, m5stickv, tdata
+):
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_SINGLESIG, TYPE_MULTISIG
from krux.qr import FORMAT_NONE, FORMAT_PMOFN
wallet = Wallet(tdata.MULTISIG_NATIVE_SW_1)
@@ -1195,48 +1246,126 @@ def test_load_multisig_fails_with_singlesig_descriptor(mocker, m5stickv, tdata):
(tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_NONE),
]
for case in cases:
- with pytest.raises(ValueError):
+ # Should raise PolicyMismatchWarning due to policy type mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(case[0], case[1])
+ # Verify exception contains correct policy types
+ assert exc_info.value.args[0] == TYPE_SINGLESIG # required_policy_type
+ assert exc_info.value.args[2] == TYPE_MULTISIG # current_policy_type
-def test_load_multisig_fails_with_miniscript_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+ # Verify wallet can load after re-creating with correct policy
+ singlesig_key = Key(
+ tdata.MULTISIG_NATIVE_SW_1.mnemonic,
+ TYPE_SINGLESIG,
+ tdata.MULTISIG_NATIVE_SW_1.network,
+ tdata.MULTISIG_NATIVE_SW_1.passphrase,
+ )
+ wallet_with_correct_policy = Wallet(singlesig_key)
+ wallet_with_correct_policy.load(case[0], case[1])
+ assert wallet_with_correct_policy.is_loaded()
+
+
+def test_load_multisig_warns_mismatch_with_miniscript_descriptor(
+ mocker, m5stickv, tdata
+):
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_MINISCRIPT, TYPE_MULTISIG, P2WSH, P2TR
from krux.qr import FORMAT_NONE
wallet = Wallet(tdata.MULTISIG_NATIVE_SW_1)
- with pytest.raises(ValueError):
+ # Test P2WSH miniscript
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
- with pytest.raises(ValueError):
+ assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type
+ assert exc_info.value.args[1] == P2WSH # required_script_type
+ assert exc_info.value.args[2] == TYPE_MULTISIG # current_policy_type
+
+ # Verify wallet can load with correct policy
+ miniscript_key = Key(
+ tdata.MULTISIG_NATIVE_SW_1.mnemonic,
+ TYPE_MINISCRIPT,
+ tdata.MULTISIG_NATIVE_SW_1.network,
+ tdata.MULTISIG_NATIVE_SW_1.passphrase,
+ script_type=P2WSH,
+ )
+ wallet_wsh = Wallet(miniscript_key)
+ wallet_wsh.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ assert wallet_wsh.is_loaded()
+
+ # Test P2TR miniscript
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type
+ assert exc_info.value.args[1] == P2TR # required_script_type
+ assert exc_info.value.args[2] == TYPE_MULTISIG # current_policy_type
+ # Verify wallet can load with correct policy
+ tap_miniscript_key = Key(
+ tdata.MULTISIG_NATIVE_SW_1.mnemonic,
+ TYPE_MINISCRIPT,
+ tdata.MULTISIG_NATIVE_SW_1.network,
+ tdata.MULTISIG_NATIVE_SW_1.passphrase,
+ script_type=P2TR,
+ )
+ wallet_tr = Wallet(tap_miniscript_key)
+ wallet_tr.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ assert wallet_tr.is_loaded()
-def test_load_miniscript_fails_with_singlesig_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+
+def test_load_miniscript_warns_mismatch_with_singlesig_descriptor(
+ mocker, m5stickv, tdata
+):
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_SINGLESIG, TYPE_MINISCRIPT, P2WPKH
from krux.qr import FORMAT_NONE, FORMAT_PMOFN
- wallets = [
- Wallet(tdata.MINISCRIPT_KEY),
- Wallet(tdata.TAP_MINISCRIPT_KEY),
+ miniscript_keys = [
+ tdata.MINISCRIPT_KEY,
+ tdata.TAP_MINISCRIPT_KEY,
]
cases = [
(tdata.SPECTER_SINGLESIG_WALLET_DATA, FORMAT_PMOFN),
(tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_NONE),
]
- for wallet in wallets:
+
+ for miniscript_key in miniscript_keys:
+ wallet = Wallet(miniscript_key)
for case in cases:
- with pytest.raises(ValueError):
+ # Should raise PolicyMismatchWarning due to policy type mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(case[0], case[1])
+ # Verify exception contains correct policy types
+ assert exc_info.value.args[0] == TYPE_SINGLESIG # required_policy_type
+ assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type
+
+ # Verify wallet can load after re-creating with correct policy
+ singlesig_key = Key(
+ miniscript_key.mnemonic,
+ TYPE_SINGLESIG,
+ network=miniscript_key.network,
+ passphrase=miniscript_key.passphrase,
+ account_index=miniscript_key.account_index,
+ script_type=P2WPKH,
+ )
+ wallet_with_correct_policy = Wallet(singlesig_key)
+ wallet_with_correct_policy.load(case[0], case[1])
+ assert wallet_with_correct_policy.is_loaded()
-def test_load_miniscript_fails_with_multisig_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+
+def test_load_miniscript_warns_mismatch_with_multisig_descriptor(
+ mocker, m5stickv, tdata
+):
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_MULTISIG, TYPE_MINISCRIPT, P2WSH
from krux.qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR
- wallets = [
- Wallet(tdata.MINISCRIPT_KEY),
- Wallet(tdata.TAP_MINISCRIPT_KEY),
+ miniscript_keys = [
+ tdata.MINISCRIPT_KEY,
+ tdata.TAP_MINISCRIPT_KEY,
]
cases = [
@@ -1246,35 +1375,100 @@ def test_load_miniscript_fails_with_multisig_descriptor(mocker, m5stickv, tdata)
(tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR),
]
- for wallet in wallets:
+ for miniscript_key in miniscript_keys:
+ wallet = Wallet(miniscript_key)
for case in cases:
- with pytest.raises(ValueError):
+ # Should raise PolicyMismatchWarning due to policy type mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(case[0], case[1])
+ # Verify exception contains correct policy types
+ required_policy_type = exc_info.value.args[0]
+ assert required_policy_type == TYPE_MULTISIG
+ assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type
+
+ # Note: These descriptors don't contain this key's xpub as a cosigner,
+ # so even after fixing the policy type, loading would fail with
+ # "xpub not a multisig cosigner" error
-def test_load_wsh_miniscript_fails_with_tr_miniscript_descriptor(
+
+def test_load_wsh_miniscript_warns_mismatch_with_tr_miniscript_descriptor(
mocker, m5stickv, tdata
):
- from krux.wallet import Wallet
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_MINISCRIPT, P2WSH, P2TR
from krux.qr import FORMAT_NONE
wallet = Wallet(tdata.MINISCRIPT_KEY)
- with pytest.raises(ValueError):
+ # Should raise PolicyMismatchWarning due to script type mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ # Verify exception contains correct policy and script types
+ required_policy_type = exc_info.value.args[0]
+ required_script_type = exc_info.value.args[1]
+ assert required_policy_type == TYPE_MINISCRIPT
+ assert required_script_type == P2TR
+ assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type
+
+ # Extract required network if present in exception
+ required_network = exc_info.value.args[3] if len(exc_info.value.args) > 3 else None
+
+ # Verify wallet can load after re-creating with correct script type and network
+ corrected_key = Key(
+ tdata.MINISCRIPT_KEY.mnemonic,
+ required_policy_type,
+ network=required_network if required_network else tdata.MINISCRIPT_KEY.network,
+ passphrase=tdata.MINISCRIPT_KEY.passphrase,
+ account_index=tdata.MINISCRIPT_KEY.account_index,
+ script_type=required_script_type,
+ )
+ wallet_with_correct_script = Wallet(corrected_key)
+ wallet_with_correct_script.load(
+ tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE
+ )
+ assert wallet_with_correct_script.is_loaded()
-def test_load_tr_miniscript_fails_with_wsh_miniscript_descriptor(
+
+def test_load_tr_miniscript_warns_mismatch_with_wsh_miniscript_descriptor(
mocker, m5stickv, tdata
):
- from krux.wallet import Wallet
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key, TYPE_MINISCRIPT, P2WSH, P2TR
from krux.qr import FORMAT_NONE
wallet = Wallet(tdata.TAP_MINISCRIPT_KEY)
- with pytest.raises(ValueError):
+ # Should raise PolicyMismatchWarning due to script type mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ # Verify exception contains correct policy and script types
+ required_policy_type = exc_info.value.args[0]
+ required_script_type = exc_info.value.args[1]
+ assert required_policy_type == TYPE_MINISCRIPT
+ assert required_script_type == P2WSH
+ assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type
+
+ # Extract required network if present in exception
+ required_network = exc_info.value.args[3] if len(exc_info.value.args) > 3 else None
+
+ # Verify wallet can load after re-creating with correct script type and network
+ corrected_key = Key(
+ tdata.TAP_MINISCRIPT_KEY.mnemonic,
+ required_policy_type,
+ network=(
+ required_network if required_network else tdata.TAP_MINISCRIPT_KEY.network
+ ),
+ passphrase=tdata.TAP_MINISCRIPT_KEY.passphrase,
+ account_index=tdata.TAP_MINISCRIPT_KEY.account_index,
+ script_type=required_script_type,
+ )
+ wallet_with_correct_script = Wallet(corrected_key)
+ wallet_with_correct_script.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ assert wallet_with_correct_script.is_loaded()
+
def test_load_singlesig_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata):
from krux.wallet import Wallet
@@ -1297,59 +1491,83 @@ def test_load_multisig_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata)
def test_load_miniscript_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key
from krux.qr import FORMAT_NONE
wallet = Wallet(tdata.MINISCRIPT_KEY)
- with pytest.raises(ValueError):
+ # First, should raise PolicyMismatchWarning due to network mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.UNRELATED_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ # Extract required policy, script type, and network from exception
+ required_policy_type = exc_info.value.args[0]
+ required_script_type = exc_info.value.args[1]
+ required_network = (
+ exc_info.value.args[3]
+ if len(exc_info.value.args) > 3
+ else tdata.MINISCRIPT_KEY.network
+ )
+
+ # Create key with correct network but still unrelated to descriptor
+ corrected_key = Key(
+ tdata.MINISCRIPT_KEY.mnemonic,
+ required_policy_type,
+ network=required_network if required_network else tdata.MINISCRIPT_KEY.network,
+ passphrase=tdata.MINISCRIPT_KEY.passphrase,
+ account_index=tdata.MINISCRIPT_KEY.account_index,
+ script_type=required_script_type,
+ )
+ wallet_corrected = Wallet(corrected_key)
+
+ # Now should raise ValueError because key is not in descriptor
+ with pytest.raises(ValueError):
+ wallet_corrected.load(tdata.UNRELATED_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+
def test_load_tr_miniscript_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata):
- from krux.wallet import Wallet
+ from krux.wallet import Wallet, PolicyMismatchWarning
+ from krux.key import Key
from krux.qr import FORMAT_NONE
wallet = Wallet(tdata.TAP_MINISCRIPT_KEY)
- with pytest.raises(ValueError):
+ # First, should raise PolicyMismatchWarning due to network mismatch
+ with pytest.raises(PolicyMismatchWarning) as exc_info:
wallet.load(tdata.UNRELATED_TAP_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+ # Extract required policy, script type, and network from exception
+ required_policy_type = exc_info.value.args[0]
+ required_script_type = exc_info.value.args[1]
+ required_network = (
+ exc_info.value.args[3]
+ if len(exc_info.value.args) > 3
+ else tdata.TAP_MINISCRIPT_KEY.network
+ )
+
+ # Create key with correct network but still unrelated to descriptor
+ corrected_key = Key(
+ tdata.TAP_MINISCRIPT_KEY.mnemonic,
+ required_policy_type,
+ network=(
+ required_network if required_network else tdata.TAP_MINISCRIPT_KEY.network
+ ),
+ passphrase=tdata.TAP_MINISCRIPT_KEY.passphrase,
+ account_index=tdata.TAP_MINISCRIPT_KEY.account_index,
+ script_type=required_script_type,
+ )
+ wallet_corrected = Wallet(corrected_key)
+
+ # Now should raise ValueError because key is not in descriptor
+ with pytest.raises(ValueError):
+ wallet_corrected.load(tdata.UNRELATED_TAP_MINISCRIPT_DESCRIPTOR, FORMAT_NONE)
+
def test_parse_wallet(mocker, m5stickv, tdata):
- from krux.wallet import parse_wallet, AssumptionWarning
+ from krux.wallet import parse_wallet
cases = [
- (
- tdata.KRUX_LEGACY1_XPUB,
- tdata.KRUX_LEGACY1_DESCRIPTOR,
- None,
- ),
- (
- tdata.KRUX_NESTEDSW1_XPUB,
- tdata.KRUX_NESTEDSW1_DESCRIPTOR,
- None,
- ),
- (
- tdata.KRUX_NESTEDSW1_YPUB,
- tdata.KRUX_NESTEDSW1_YPUB_DESCRIPTOR,
- None,
- ),
- (
- tdata.KRUX_NATIVESW1_XPUB,
- tdata.KRUX_NATIVESW1_DESCRIPTOR,
- None,
- ),
- (
- tdata.KRUX_NATIVESW1_ZPUB,
- tdata.KRUX_NATIVESW1_ZPUB_DESCRIPTOR,
- None,
- ),
- (
- tdata.KRUX_TAPROOT1_XPUB,
- tdata.KRUX_TAPROOT1_DESCRIPTOR,
- None,
- ),
(
tdata.SPECTER_SINGLESIG_WALLET_DATA,
tdata.SPECTER_SINGLESIG_DESCRIPTOR,
@@ -1360,21 +1578,6 @@ def test_parse_wallet(mocker, m5stickv, tdata):
tdata.SPECTER_MULTISIG_DESCRIPTOR,
"Specter Multisig Wallet",
),
- (
- tdata.BLUEWALLET_SINGLESIG_WALLET_DATA,
- tdata.BLUEWALLET_SINGLESIG_DESCRIPTOR,
- None,
- ),
- # ( BlueWallet legacy bip44 xpub w/o key-origin is not supported, will default to bip84
- # tdata.BLUEWALLET_LEGACY_WALLET_DATA,
- # tdata.BLUEWALLET_LEGACY_DESCRIPTOR,
- # None,
- # ),
- (
- tdata.BLUEWALLET_NESTEDSW_WALLET_DATA,
- tdata.BLUEWALLET_NESTEDSW_DESCRIPTOR,
- None,
- ),
(
tdata.BLUEWALLET_MULTISIG_WALLET_DATA,
tdata.BLUEWALLET_MULTISIG_DESCRIPTOR,
@@ -1426,10 +1629,7 @@ def test_parse_wallet(mocker, m5stickv, tdata):
for case in cases:
print(case_n)
case_n += 1
- try:
- descriptor, label = parse_wallet(case[0])
- except AssumptionWarning as e:
- descriptor, label = parse_wallet(case[0], allow_assumption=e.args[1])
+ descriptor, label = parse_wallet(case[0])
assert descriptor.to_string() == case[1].split("#")[0].replace("'", "h")
assert label == case[2]
@@ -1622,139 +1822,6 @@ def test_version_to_network_versiontype():
)
-def test_xpub_data_to_derivation():
- from krux.wallet import xpub_data_to_derivation, AssumptionWarning
-
- # purpose
- LEGACY = 44 + 2**31
- NATSW = 84 + 2**31
- NESSW = 49 + 2**31
- TAPROOT = 86 + 2**31
- MULTISIG = 48 + 2**31
-
- # network
- MAIN = 0 + 2**31
- TEST = 1 + 2**31
-
- # account
- ACCT0 = 0 + 2**31
- ACCT1 = 1 + 2**31
-
- # multisig script type
- MULTINESSW = 1 + 2**31
- MULTINATSW = 2 + 2**31
-
- cases = [
- # versiontype, network, child, depth, allow_assumption, expected_return
- ("xpub", "main", ACCT0, 3, None, AssumptionWarning), # don't assume
- ("xpub", "main", ACCT0, 3, [NATSW, MAIN, ACCT0], [NATSW, MAIN, ACCT0]),
- ("xpub", "test", ACCT0, 3, [NATSW, TEST, ACCT0], [NATSW, TEST, ACCT0]),
- ("xpub", "main", ACCT1, 3, [NATSW, MAIN, ACCT1], [NATSW, MAIN, ACCT1]),
- ("xpub", "main", ACCT0, 4, [NATSW, MAIN, ACCT0], None), # wrong depth
- ("ypub", "main", ACCT0, 3, None, [NESSW, MAIN, ACCT0]),
- ("ypub", "test", ACCT0, 3, None, [NESSW, TEST, ACCT0]),
- ("ypub", "main", ACCT1, 3, None, [NESSW, MAIN, ACCT1]),
- ("ypub", "test", ACCT1, 3, None, [NESSW, TEST, ACCT1]),
- ("ypub", "main", ACCT0, 4, None, None), # wrong depth
- ("zpub", "main", ACCT0, 3, None, [NATSW, MAIN, ACCT0]),
- ("zpub", "test", ACCT0, 3, None, [NATSW, TEST, ACCT0]),
- ("zpub", "main", ACCT1, 3, None, [NATSW, MAIN, ACCT1]),
- ("zpub", "test", ACCT1, 3, None, [NATSW, TEST, ACCT1]),
- ("zpub", "main", ACCT0, 4, None, None), # wrong depth
- ("Ypub", "main", MULTINESSW, 4, None, AssumptionWarning), # don't assume
- ("Ypub", "test", MULTINESSW, 4, None, AssumptionWarning),
- (
- "Ypub",
- "main",
- MULTINESSW,
- 4,
- [MULTISIG, MAIN, ACCT0, MULTINESSW],
- [MULTISIG, MAIN, ACCT0, MULTINESSW],
- ),
- (
- "Ypub",
- "test",
- MULTINESSW,
- 4,
- [MULTISIG, TEST, ACCT0, MULTINESSW],
- [MULTISIG, TEST, ACCT0, MULTINESSW],
- ),
- ("Ypub", "main", MULTINESSW, 3, None, None), # wrong depth
- ("Zpub", "main", MULTINATSW, 4, None, AssumptionWarning), # don't assume
- ("Zpub", "test", MULTINATSW, 4, None, AssumptionWarning),
- (
- "Zpub",
- "main",
- MULTINATSW,
- 4,
- [MULTISIG, MAIN, ACCT0, MULTINATSW],
- [MULTISIG, MAIN, ACCT0, MULTINATSW],
- ),
- (
- "Zpub",
- "test",
- MULTINATSW,
- 4,
- [MULTISIG, TEST, ACCT0, MULTINATSW],
- [MULTISIG, TEST, ACCT0, MULTINATSW],
- ),
- ("Zpub", "main", MULTINATSW, 3, None, None), # wrong depth
- ]
-
- for _case in cases:
- if type(_case[5]) == type and issubclass(_case[5], Exception):
- with pytest.raises(_case[5]):
- xpub_data_to_derivation(*_case[:5])
- else:
- assert xpub_data_to_derivation(*_case[:5]) == _case[5]
-
-
-def test_derivation_to_script_wrapper():
- from krux.wallet import derivation_to_script_wrapper
-
- # purpose
- LEGACY = 44 + 2**31
- NATSW = 84 + 2**31
- NESSW = 49 + 2**31
- TAPROOT = 86 + 2**31
-
- # network
- MAIN = 0 + 2**31
- TEST = 1 + 2**31
-
- # account
- HARD = 0 + 2**31
-
- NOTHARD = 2**31 - 1
-
- cases = [
- # derivation list as the only function param and expected return
- ([HARD, MAIN], None),
- ([HARD, TEST], None),
- ([HARD, MAIN, HARD], None),
- ([NOTHARD, MAIN, HARD], None),
- ([LEGACY, MAIN, HARD], "pkh({})"),
- ([LEGACY, TEST, HARD], "pkh({})"),
- ([LEGACY, MAIN, NOTHARD], None),
- ([LEGACY, NOTHARD, HARD], None),
- ([NESSW, MAIN, HARD], "sh(wpkh({}))"),
- ([NESSW, TEST, HARD], "sh(wpkh({}))"),
- ([NESSW, MAIN, NOTHARD], None),
- ([NESSW, NOTHARD, HARD], None),
- ([NATSW, MAIN, HARD], "wpkh({})"),
- ([NATSW, TEST, HARD], "wpkh({})"),
- ([NATSW, MAIN, NOTHARD], None),
- ([NATSW, NOTHARD, HARD], None),
- ([TAPROOT, MAIN, HARD], "tr({})"),
- ([TAPROOT, TEST, HARD], "tr({})"),
- ([TAPROOT, MAIN, NOTHARD], None),
- ([TAPROOT, NOTHARD, HARD], None),
- ]
-
- for _case in cases:
- assert derivation_to_script_wrapper(_case[0]) == _case[1]
-
-
def test_parse_wallet_via_ur_output(mocker, m5stickv):
from krux.wallet import parse_wallet
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.