{
 "number": 36583,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36583",
 "title": "wallet: fix migration of watch-only wallets with transactions",
 "author": "yusufyildiz34",
 "author_association": "FIRST_TIME_CONTRIBUTOR",
 "created_at": "2026-10-20T12:08:50Z",
 "updated_at": "2026-10-26T09:52:20Z",
 "description_hash": "c3ef4957dd00b4ca",
 "age_days": 12,
 "draft": false,
 "labels": [
  "Wallet"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "50f254e53ac357e5a8315062c634e36071e1aa19",
 "head_ref": "wallet-migration-36483",
 "head_repo": "yusufyildiz34/bitcoin",
 "head_history": [],
 "additions": 64,
 "deletions": 3,
 "changed_files": 5,
 "commit_count": 2,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "Naomi-Gift",
      "url": "https://github.com/bitcoin/bitcoin/pull/36583#issuecomment-6334608827"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "Naomi-Gift": {
   "kind": "ack",
   "hash": "50f254e53ac357e5a8315062c634e36071e1aa19",
   "t": "2026-10-22T06:47:34Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Naomi-Gift"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-20T11:32:00Z",
  "last_reviewer_activity": "2026-10-22T06:47:34Z",
  "last_reviewer": "Naomi-Gift",
  "author_silent_days": 12,
  "waiting_on_author_days": 10,
  "days_since_update": 6
 },
 "refs": {
  "mentioned": [
   36483
  ],
  "depends_on": [],
  "fixes": [
   36483
  ],
  "linked_issues": [
   {
    "number": 36483,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Migration fails for legacy wallet with no keys watching an address"
   }
  ],
  "references": [
   {
    "number": 36483,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Migration fails for legacy wallet with no keys watching an address"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "Migrating a legacy wallet that holds no spendable material fails whenever the\nwallet contains transactions. `migratewallet` returns\n`std::bad_function_call (-1)` and the wallet is left unmigrated.\n\nTwo defects are involved, and the first one hides the second.\n\n### 1. The migration check reads in-memory state that has not been updated yet\n\nWhen the wallet being migrated has no spendable material,\n`CWallet::ApplyMigrationData()` asserts that nothing is left behind in it:\n\n```cpp\nif (!m_address_book.empty()) return util::Error{_(\"Error: Not all address book records were migrated\")};\nif (!mapWallet.empty()) return util::Error{_(\"Error: Not all transaction records were migrated\")};\n```\n\nThe two checks look symmetric but are not. `DelAddressBookWithDB()` erases from\n`m_address_book` straight away, so the first check sees current state.\n`RemoveTxs()` only erases the transaction records from disk; it defers the\n`mapWallet` removal to the db txn's `on_commit` listener, which does not run\nuntil the txn is committed \u2014 after `ApplyMigrationData()` has returned.\n\n`mapWallet` is therefore never empty at that point and the check always fails.\n\n### 2. Aborting the txn calls a callback that was never set\n\n`DbTxnListener` declares `on_commit` and `on_abort` as plain `std::function`\nmembers and callers may set only the one they need. `RemoveTxs()` does exactly\nthat: `on_commit` is set, `on_abort` is left empty.\n\n`TxnCommit()` and `TxnAbort()` each invoked their listener's callback\nunconditionally, so the abort triggered by the failed check above threw\n`std::bad_function_call`. The exception escaped the wallet entirely, replacing\nthe real error with an unhelpful one and skipping the caller's cleanup path.\n\n### Fix\n\nThe first commit skips callbacks that are not set, and documents that they are\noptional. The second discounts the transactions already queued for deletion\ninstead of requiring `mapWallet` to be empty.\n\nThey are kept separate because the second defect masks the first: with only the\nlistener fix applied, the migration surfaces the real error\n`Error: Not all transaction records were migrated (-4)` instead of\n`std::bad_function_call (-1)`.\n\n### Testing\n\n`src/wallet/test/db_tests.cpp` gains a regression test covering a listener with\nonly one callback set, for both commit and abort. It fails on master with\n`std::bad_function_call` and passes with the fix.\n\n`test/functional/wallet_migration.py` gains a case migrating a blank legacy\nwallet with a watch-only address that has received a transaction. The test is\ntaken from #36483.\n\nTo reproduce on master, apply the functional test alone and run\n`wallet_migration.py`.\n\nFixes #36483",
 "commits": [
  {
   "sha": "72889f7e1e9adaba4a449cbacca72a58e2b36bcf",
   "date": "2026-10-20T11:32:00Z",
   "message": "wallet: skip unset db txn listener callbacks\n\nDbTxnListener declares on_commit and on_abort as plain std::function\nmembers and callers may set only the one they need. RemoveTxs() does\nexactly that: it registers a listener with on_commit set and on_abort\nleft empty.\n\nTxnCommit() and TxnAbort() invoked both unconditionally, so aborting a\ndb txn in which RemoveTxs() had run threw std::bad_function_call. The\nexception escaped the wallet, replacing the error that caused the abort\nwith an unhelpful \"std::bad_function_call\" RPC error and skipping the\ncaller's cleanup path.\n\nSkip callbacks that are not set, and document that they are optional."
  },
  {
   "sha": "50f254e53ac357e5a8315062c634e36071e1aa19",
   "date": "2026-10-20T11:32:00Z",
   "message": "wallet: fix migration of watch-only wallets\n\nWhen the wallet being migrated has no spendable material,\nApplyMigrationData() asserts that no records are left behind in it. The\naddress book half of that check works because DelAddressBookWithDB()\nerases from m_address_book straight away. The transaction half cannot:\nRemoveTxs() only erases the records from disk and defers the mapWallet\nremoval to the db txn's on_commit listener, which runs after\nApplyMigrationData() has returned.\n\nmapWallet is therefore never empty at that point, so migrating a\nwatch-only wallet that has transactions always failed with\n\"Error: Not all transaction records were migrated\".\n\nDiscount the transactions queued for deletion instead."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-22T06:47:34Z",
   "kind": "comment",
   "who": "Naomi-Gift",
   "assoc": "NONE",
   "text": "utACK 50f254e53ac357e5a8315062c634e36071e1aa19",
   "id": "c:6334608827",
   "url": "https://github.com/bitcoin/bitcoin/pull/36583#issuecomment-6334608827",
   "hash": "ce58fb57eb581b67",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-20T12:08:55Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 4125,
 "text_tokens_estimate": 1031,
 "changed_paths": [
  "src/wallet/test/db_tests.cpp",
  "src/wallet/wallet.cpp",
  "src/wallet/walletdb.cpp",
  "src/wallet/walletdb.h",
  "test/functional/wallet_migration.py"
 ],
 "files": [
  {
   "path": "src/wallet/test/db_tests.cpp",
   "add": 35,
   "del": 0
  },
  {
   "path": "src/wallet/wallet.cpp",
   "add": 5,
   "del": 1
  },
  {
   "path": "src/wallet/walletdb.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/walletdb.h",
   "add": 2,
   "del": 0
  },
  {
   "path": "test/functional/wallet_migration.py",
   "add": 20,
   "del": 0
  }
 ],
 "test_lines": 55,
 "git": {
  "head": "50f254e53ac357e5a8315062c634e36071e1aa19",
  "head_matches_backup": true,
  "base": "6799119cef7ead71e2b2fc7928a0ea58d720c708",
  "patch_id": "1c222ae351acf073",
  "commits": [
   {
    "sha": "72889f7e1e",
    "subject": "wallet: skip unset db txn listener callbacks",
    "files": 3,
    "add": 39,
    "del": 2
   },
   {
    "sha": "50f254e53a",
    "subject": "wallet: fix migration of watch-only wallets",
    "files": 2,
    "add": 25,
    "del": 1
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "1c222ae351acf073",
 "input_hash": "4385c6daa3a6af11",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}