Skip to content

feat: back up hardware wallet names - #681

Draft
jvsena42 wants to merge 7 commits into
masterfrom
feat/hw-wallet-name-backup
Draft

feat: back up hardware wallet names#681
jvsena42 wants to merge 7 commits into
masterfrom
feat/hw-wallet-name-backup

Conversation

@jvsena42

Copy link
Copy Markdown
Member

This PR:

  1. Backs up the name you give a hardware wallet, and restores it when the device is paired again.
  2. Asks, when removing a hardware wallet, whether to keep its name and tags in the backup, defaulting to keeping them.

Ports synonymdev/bitkit-android#1173, the follow-up to bitkit-android#1163 that this repo ported in #675.

Description

#675 made hardware wallet tags survive a restore: they ride the metadata backup as pre-activity metadata keyed by the Core-derived wallet id, and Core re-attaches them once the device watcher recreates the activities. Two gaps were left behind, and both exist here exactly as they did on Android.

The name itself was never backed up. It lives only on the local device entry, so restoring on a new phone and pairing the Trezor again brought the tags back but not the name the user chose. Nothing even marked a backup as required when a name changed, so a rename was invisible to the backup system entirely.

The second gap is the more damaging one. Removing a hardware wallet destroyed its tags in the backup, silently. Core deletes a wallet's activities, its activity tags and its pre-activity metadata in one cascade, which is both of the sources the metadata envelope draws hardware tags from, and the deletion then signals that tags changed, so a metadata envelope without them was uploaded within seconds. The dialog said only that funds are safe and coins will not be deleted; it never mentioned the tags. A full wallet wipe was never affected, because wiping suppresses uploads and leaves the stored backup intact.

The name travels on the same key the tags already use. Core's wallet id derives from a device's account extended public keys, is stable across installs and platforms, and does not depend on the order of those keys, so a name keyed by it survives a restore exactly as well as the tags do.

The name of a paired wallet still lives on its device entry, which is untouched. What is new is a small pending map for the two moments when no device entry exists: a name restored from a backup before the device has been paired again, and a name kept when a wallet was removed. Pairing adopts a pending name into the entry's own label, and a wallet the device list already names is masked out of the pending set rather than pruned from it, so consuming a name needs no second write that could be lost on its own.

Removal takes the name and tag snapshot before the cascade and writes the tags back after it, carrying the name in the same store write that forgets the device entries. The two kinds of data are kept independently: a wallet that was never renamed still keeps its tags, which is the more common shape by far. Turning the toggle off is the current behaviour made explicit: the tags go, and any name kept from an earlier removal of the same wallet is dropped too.

The write-back is the tail of every delete of that wallet rather than a step in the removal alone. Core drops a wallet's pre-activity metadata along with its activities whether or not any matched, so a cleanup delete arriving later repairs itself instead of taking the rows a removal deliberately kept, and a later removal that keeps nothing is what clears the repair.

Two failure points are treated differently on purpose, because the cascade sits between them. Failing to read the tags happens before anything is deleted, so the wallet is untouched and the removal is refused with a message naming the way through — retry, or remove without keeping the data. Failing to write them back happens after the activities are already gone and the watchers already stopped, where there is nothing to roll back to and reporting a failed removal would be false, so that one is logged and the removal completes.

Two pre-existing bugs surfaced while wiring this up and are fixed here. Removing a wallet never dropped it from the manager's device list, so the push that follows deleted its activities a second time, and the device grouping kept yielding the removed group long enough for a watcher event still in flight to re-persist the activities that had just been deleted. The wallet now leaves the list synchronously, inside the removal. The existing test asserting that a removal deletes a wallet's activities exactly once passed only because it ran without a device session, so no push ever happened; it now performs that push.

One behaviour falls out of this for free: re-pairing a wallet that was removed with the toggle on restores its tags with no restore flow involved at all, because the pre-activity metadata rows survived locally and Core re-attaches them as the watcher recreates the activities.

The envelope is shared with Android, and this adds a field to it rather than reusing one, so both sides need it. Swift ignores unknown keys when decoding but drops them when it re-encodes, and this app re-uploads the metadata envelope often, so until this ships a user running both apps on one seed would lose their backed-up names. The wallet-scoped tag read that removal uses orders rendered tags ahead of stored rows, the opposite of the Android equivalent and for the same reason already documented for the envelope build here: a stored row can outlive the activity it was meant for and hold tags the user has since edited, and keeping a wallet's tags means keeping what the user currently sees.

The remove confirmation is a card rather than a native alert, which cannot hold a switch. There was no existing dialog component and no design for this screen, so it is built from the app's own tokens in the shape the Android dialog uses.

Envelope format is otherwise unchanged and bitkit-core is not bumped.

Companion Android PR: synonymdev/bitkit-android#1173

Linked Issues/Tasks

Screenshot / Video

QA Notes

Requires a paired Trezor (the bitkit-docker emulator is enough) and a wallet with existing hardware activity. Manual tests below have not been run yet.

Manual Tests

  • 1. Hardware Wallet → rename the wallet → Settings → Data Backups: Tags shows as synced again.
  • 2. regression: Disconnect and reconnect the device without renaming → Settings → Data Backups: Tags is not re-uploaded.
  • 3. Rename a hardware wallet and tag one of its transactions → wipe wallet → restore from seed → pair the device again: the tile shows the custom name and the tagged transaction shows its tags.
  • 4a. Hardware Wallet → Remove with the keep toggle on → pair the device again: name and tags return.
    • 4b. Remove with the keep toggle off → pair the device again: the wallet comes back under the device's own name with no tags.
    • 4c. Never-renamed wallet with tagged transactions → Remove with the keep toggle on → pair the device again: the tags return.
  • 5. Settings → Hardware Wallets → trash icon: the same dialog opens, with the toggle on by default.
  • 6. Remove dialog → turn the toggle off → cancel → reopen the dialog: the toggle is on again.
  • 7. Passphrase wallet → Remove with the keep toggle on → pair again with the same passphrase: its own name returns, and the standard wallet's name is unaffected.
  • 8. regression: Remove dialog → cancel: the wallet stays paired with its name and tags.

Automated Checks

  • Unit tests added: pending names in BitkitTests/TrezorKnownDeviceStorageTests.swift, covering a pending name and the device list written together, a paired wallet masking its own pending entry, a paired name winning over a pending one, a restore letting a local name win and never clearing on an envelope without the field, forgetting a wallet dropping the name kept for it, a wipe clearing both keys, and the backup signal firing on a rename but not on a reconnect.
  • Unit tests added: removal in BitkitTests/HwWalletManagerTests.swift, covering the tag write-back following Core's cascade, a later cleanup delete re-applying the kept rows, a removal deleting a wallet's activities exactly once, the toggle-off branch reading and writing nothing, an unreadable tag snapshot refusing the removal with the wallet left entirely untouched, a removal surviving a failed write-back, a keep-off removal disarming an earlier repair, and a watcher event after removal no longer re-persisting the wallet.
  • Unit tests modified: the existing removal test in BitkitTests/HwWalletManagerTests.swift now performs the device push that follows a removal. Without it the test passed vacuously, which is what hid the double delete.
  • Unit tests modified: the passphrase mismatch cleanup in BitkitTests/HwWalletManagerPassphraseTests.swift now asserts that the stray wallet a typo opened keeps its backup data, since storing it already consumed any name restored for it.
  • Unit tests added: envelope round-trip in BitkitTests/PubkyProfileManagerTests.swift, alongside the existing payload tests, covering the new field surviving a round trip and an envelope written without it decoding to nil rather than an explicit null.
  • The watcher-cleanup interleaving behind the double-removal case needs real concurrency and is not reproducible on the test dispatcher. What is unit tested is the resulting rule: a delete of a wallet whose data was kept re-applies it.
  • Test target coverage: 904 unit tests pass locally on iPhone 16, with AddressTypeIntegrationTests skipped. The 2 failures are UtxoSelectionTests failing on Failed to deposit to address: Blocktank error, which needs the bitkit-docker regtest stack; AddressTypeIntegrationTests fails for the same reason. Both are unrelated to this change.
  • node scripts/validate-translations.js: 0 errors, 3 new strings.
  • swiftformat .: clean.

@jvsena42 jvsena42 self-assigned this Aug 21, 2026
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.

1 participant