Skip to content

feat: add native seedqr wallet restore - #760

Merged
pwltr merged 1 commit into
masterfrom
codex/seedqr-import
Sep 22, 2026
Merged

pwltr merged 1 commit into
masterfrom
codex/seedqr-import

Conversation

@pwltr

@pwltr pwltr commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds native SeedQR wallet restoration on iOS.

  • Adds a scanner action to the existing 12-word wallet restore screen and fills the current recovery form after successful decoding.
  • Supports Standard SeedQR and Compact SeedQR, including the QR-framed binary payload returned by Vision on physical devices.
  • Replaces CodeScanner with an AVFoundation and Vision implementation so binary QR payloads are available without another scanner dependency.
  • Uses the shared SeedQR decoder released in bitkit-core v0.5.18; iOS retains only camera capture and Vision-specific payload normalization.
  • Keeps camera and photo-library processing local and does not log or persist scanned recovery data.
  • Preserves the existing payment and probing-tool scanner flows through a shared text-and-binary QR payload.

SeedQR originated in SeedSigner, but it is not limited to one product: Specter DIY independently supports both Standard and Compact SeedQR. SeedQR is not itself a BIP; it is a compact transport encoding of BIP39 word indexes or entropy. Supporting it therefore improves interoperability with existing air-gapped backup tooling without introducing a new wallet or seed derivation scheme.

Linked Issues/Tasks

Design

N/A — no design available.

Screenshot / Video

N/A — no recovery seed material was captured.

QA Notes

Manual Tests

  • 1. Restore Wallet → scan a Standard SeedQR on a physical iPhone: the twelve recovery words populate the existing form.
  • 2. Restore Wallet → scan a Compact SeedQR on a physical iPhone: the twelve recovery words populate the existing form.
  • 3. Restore Wallet → scan an invalid or unsupported QR: an error toast appears and no recovery words change.
  • 4. regression: Send → scan a payment QR: the existing payment flow opens normally.
  • 5. regression: Restore Wallet → enter twelve words manually: manual recovery remains unchanged.

The simulator has no camera input. Photo-library handling is not treated as a simulator compatibility requirement; physical-device camera tests are the authoritative checks for this feature.

Automated Checks

  • SeedQRCodeDecoderTests.swift covers Standard SeedQR, raw Compact entropy, the binary bitstream returned by Vision on a physical iPhone, null-byte entropy, invalid checksums, out-of-range indexes, and unsupported payload lengths.
  • All 8 focused SeedQR tests pass on the iOS simulator against the published bitkit-core v0.5.18 Swift package.
  • SwiftPM resolves bitkit-core v0.5.18 at merge commit b53fa54a16f04940943072bb6e08dc2650302f93.
  • An embedded Compact SeedQR fixture passed through Vision and decoded to the expected mnemonic on a physical iPhone during development; the fixture-only diagnostic was removed afterward.
  • SwiftFormat, translation validation, package resolution JSON validation, and whitespace checks pass.

@pwltr
pwltr marked this pull request as ready for review September 21, 2026 15:28
@greptile-apps

greptile-apps Bot commented Sep 21, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

The PR appears safe to merge, with a non-blocking edge case where an early torch toggle can be visually enabled without activating the camera light.

Findings

  1. P2 Torch Toggle Can Be Lost

Summary

This PR replaces the third-party QR scanner with an AVFoundation/Vision implementation and adds Standard and Compact SeedQR restoration through the shared bitkit-core decoder.

  • Introduces a shared text-and-binary QR payload for camera and photo-library scans.
  • Adds a SeedQR scanner entry point to the existing twelve-word restore form.
  • Preserves payment and probing-tool scanning by adapting them to textual payloads.
  • Updates bitkit-core to 0.5.18 and removes CodeScanner.
  • Adds focused decoder tests and localization for the restore flow.
  • One non-blocking torch initialization race should be addressed.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Camera frame or selected photo] --> B[Vision QR detection]
    B --> C[QRCodePayload]
    C --> D{Scanner flow}
    D -->|Wallet restore| E[SeedQRCodeDecoder]
    E --> F[bitkit-core Standard or Compact decoder]
    F --> G[Populate twelve-word restore form]
    D -->|Payment or general scan| H[ScannerManager text handling]
    D -->|Probing tool| I[Populate textual invoice]
Loading

Reviews (1) · Last reviewed commit: "chore: bump bitkit core to 0.5.18"

Comment thread Bitkit/Components/Scanner.swift

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No HIGH or MEDIUM findings. Two LOWs inline, both checked by a second pass.

Scope note: besides SeedQR, this PR replaces CodeScanner with an in-house AVFoundation/Vision scanner that every QR flow uses (Send, main, Electrum, add contact, probing tool). That is the larger part of the change, and the first inline comment is about it.

Checked and clean:

  • Standard decode. bitkit-core requires 48 digits, indices < 2048 and validate_mnemonic. Framed Compact parsing fails closed on ECI, multi-segment and 16-bit counts.
  • Key material. The payload is never logged and never pasted. The scanner view holds no mnemonic and has screenshotPreventMask(true). Back from the scanner leaves the form untouched.
  • Other scanners. Non-restore scanners only forward payload.string. Binary-only payloads get the unsupported-QR toast, and payment QR handling is unchanged.
  • Dependencies. CodeScanner is removed, and bitkit-core goes 0.5.14 → 0.5.18 (revision matches the tag). The core delta adds the SeedQR and Jade modules and tightens finalize_psbt sighash checks. No derivation changes.
  • Threading. Frame state lives on videoQueue, and the device and torch on sessionQueue.

Non-blocking: is there a Figma frame for the new SeedQR scanner screen? Link it and I'll diff the implementation against it on the next pass.

Comment thread Bitkit/Components/Scanner.swift
Comment thread Bitkit/Utilities/SeedQRCodeDecoder.swift Outdated
@pwltr
pwltr requested review from a team, ben-kaufman and piotr-iohk and removed request for a team September 22, 2026 10:38

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta since 0953483c (f7104855): no findings. Both LOWs are fixed.

  • Scan guard. ScannerManager is @MainActor, and the isHandlingScan check and set happen before the first await, so two scans cannot both pass. .send inside processScan calls processSendScan, so it cannot deadlock on its own guard. SeedQR still bypasses the manager, so an invalid SeedQR does not latch the camera.
  • Decoder. Compact now requires the framed header. The null-byte fixture (0x41 + 17 × 0x00 + 0xEC) parses as mode 4, count 16, 16 zero bytes, and the raw 16-byte entropy is rejected by testRejectsRawCompactEntropy.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

QA reviewed on f710485.

Since the review on 0953483, one commit. A scan is handled one at a time, and Compact decoding no longer accepts a raw 16-byte blob.

Ran on iPhone 17: a 12-word Standard SeedQR and the matching Compact SeedQR filled forum undo fragile fade shy sign arrest garment culture tube off merit. An invalid code and a 24-word code were rejected with "This QR code does not contain a valid 12-word SeedQR," and the words stayed as they were.

Twin synonymdev/bitkit-android#1319: same import. System Back keeps the form. Compact needed a closer hold. Its invalid and 24-word toast is the generic QR error. That note is on the Android review.

No findings.

Checked and clean

  • testRejectsRawCompactEntropy covers the 16-byte shortcut that was removed
  • The framed Compact fixture still decodes
  • An invalid SeedQR does not latch the camera

QA LGTM

@pwltr

pwltr commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Squashed commits

@pwltr
pwltr force-pushed the codex/seedqr-import branch from f710485 to 1c1c83c Compare September 22, 2026 14:26
@pwltr
pwltr enabled auto-merge (squash) September 22, 2026 14:27
@pwltr
pwltr merged commit ab09e35 into master Sep 22, 2026
13 checks passed
@pwltr
pwltr deleted the codex/seedqr-import branch September 22, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants