1extern crate alloc;
21
22use super::{v0, PALLET_MIGRATIONS_ID};
23use crate::{pallet, Pallet};
24#[cfg(feature = "try-runtime")]
25use alloc::vec::Vec;
26use codec::{Decode, Encode, MaxEncodedLen};
27use frame_support::{
28 migrations::{MigrationId, SteppedMigration, SteppedMigrationError},
29 pallet_prelude::PhantomData,
30 traits::{
31 fungible::MutateHold, Consideration, Get, GetStorageVersion, ReservableCurrency,
32 StorageVersion,
33 },
34 weights::WeightMeter,
35 BoundedVec,
36};
37
38#[derive(Encode, Decode, MaxEncodedLen, Clone, PartialEq, Eq, Debug)]
40pub enum MigrationCursor<AccountId: MaxEncodedLen> {
41 Recoverable(Option<AccountId>),
43 ActiveRecoveries(Option<(AccountId, AccountId)>),
45 Proxy(Option<AccountId>),
47}
48
49pub struct MigrateV0ToV1<T: v0::MigrationConfig>(PhantomData<T>);
62
63impl<T: v0::MigrationConfig> SteppedMigration for MigrateV0ToV1<T> {
64 type Cursor = MigrationCursor<T::AccountId>;
65 type Identifier = MigrationId<18>;
66
67 fn id() -> Self::Identifier {
68 MigrationId { pallet_id: *PALLET_MIGRATIONS_ID, version_from: 0, version_to: 1 }
69 }
70
71 fn step(
72 cursor: Option<Self::Cursor>,
73 meter: &mut WeightMeter,
74 ) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
75 if Pallet::<T>::on_chain_storage_version() != Self::id().version_from as u16 {
76 return Ok(None);
77 }
78
79 let required = T::DbWeight::get().reads_writes(2, 2);
80
81 if meter.remaining().any_lt(required) {
82 return Err(SteppedMigrationError::InsufficientWeight { required });
83 }
84
85 let mut cursor = cursor.unwrap_or(MigrationCursor::Recoverable(None));
86
87 loop {
88 if meter.try_consume(required).is_err() {
89 break;
90 }
91
92 match cursor {
93 MigrationCursor::Recoverable(last_key) => {
94 let mut iter = if let Some(ref key) = last_key {
95 v0::Recoverable::<T>::iter_from_key(key)
96 } else {
97 v0::Recoverable::<T>::iter()
98 };
99
100 if let Some((account, config)) = iter.next() {
101 let _ = <T as v0::MigrationConfig>::Currency::unreserve(
103 &account,
104 config.deposit,
105 );
106
107 let mut sorted_friends = config.friends.to_vec();
110 sorted_friends.sort();
111 let inheritor =
112 v0::multi_account_id::<T::AccountId>(&sorted_friends, config.threshold);
113 let friend_group = config.into_v1_friend_group(inheritor);
114
115 let friend_groups = BoundedVec::try_from(alloc::vec![friend_group])
116 .expect("ensured by integrity_test; qed");
117 let footprint = Pallet::<T>::friend_group_footprint(&friend_groups);
118
119 match T::FriendGroupsConsideration::new(&account, footprint) {
120 Ok(ticket) => {
121 pallet::FriendGroups::<T>::insert(
122 &account,
123 (friend_groups, ticket),
124 );
125 },
126 Err(_) => {
127 frame_support::defensive!(
128 "MigrateV0ToV1: Failed to create FriendGroups ticket, skipping"
129 );
130 },
131 }
132
133 v0::Recoverable::<T>::remove(&account);
134 cursor = MigrationCursor::Recoverable(Some(account));
135 } else {
136 cursor = MigrationCursor::ActiveRecoveries(None);
137 }
138 },
139 MigrationCursor::ActiveRecoveries(last_key) => {
140 let mut iter = if let Some((ref lost, ref rescuer)) = last_key {
141 v0::ActiveRecoveries::<T>::iter_from(
142 v0::ActiveRecoveries::<T>::hashed_key_for(lost, rescuer),
143 )
144 } else {
145 v0::ActiveRecoveries::<T>::iter()
146 };
147
148 let Some((lost, rescuer, recovery)) = iter.next() else {
149 cursor = MigrationCursor::Proxy(None);
150 continue;
151 };
152
153 cursor =
154 MigrationCursor::ActiveRecoveries(Some((lost.clone(), rescuer.clone())));
155 v0::ActiveRecoveries::<T>::remove(&lost, &rescuer);
156
157 let _ =
159 <T as v0::MigrationConfig>::Currency::unreserve(&rescuer, recovery.deposit);
160
161 let Some((friend_groups, _)) = pallet::FriendGroups::<T>::get(&lost) else {
163 frame_support::defensive!(
164 "MigrateV0ToV1: Failed to find FriendGroups for lost account"
165 );
166 continue;
167 };
168
169 if friend_groups.len() != 1 {
170 frame_support::defensive!(
171 "MigrateV0ToV1: Expected exactly one friend group for lost account"
172 );
173 continue;
174 }
175
176 let Some(fg) = friend_groups.first() else {
177 frame_support::defensive!(
178 "MigrateV0ToV1: Failed to find friend group for lost account"
179 );
180 continue;
181 };
182
183 let mut approvals = crate::ApprovalBitfieldOf::<T>::default();
185 for voucher in recovery.friends.iter() {
186 if let Some(index) = fg.friends.iter().position(|f| f == voucher) {
187 let _ = approvals.set_if_not_set(index);
188 } else {
189 frame_support::defensive!(
190 "MigrateV0ToV1: Voucher not found in friend group"
191 );
192 continue;
193 }
194 }
195
196 let attempt = crate::AttemptOf::<T> {
197 friend_group_index: 0, initiator: rescuer.clone(),
199 init_block: recovery.created,
200 last_approval_block: recovery.created,
201 approvals,
202 };
203
204 let security_deposit = T::SecurityDeposit::get();
205 let ticket = match crate::AttemptTicketOf::<T>::new(
206 &rescuer,
207 Pallet::<T>::attempt_footprint(),
208 ) {
209 Ok(ticket) => ticket,
210 Err(e) => {
211 log::error!(
212 "MigrateV0ToV1: Failed to create Attempt ticket for rescuer {:?}: {:?}",
213 rescuer,
214 e,
215 );
216 crate::IdentifiedConsideration {
217 depositor: rescuer.clone(),
218 ticket: None,
219 _phantom: Default::default(),
220 }
221 },
222 };
223
224 let held_deposit = if <T as pallet::Config>::Currency::hold(
225 &crate::HoldReason::SecurityDeposit.into(),
226 &rescuer,
227 security_deposit,
228 )
229 .is_ok()
230 {
231 security_deposit
232 } else {
233 log::warn!(
234 "MigrateV0ToV1: Failed to hold security deposit for rescuer; \
235 inserting Attempt with zero deposit"
236 );
237 Default::default()
238 };
239
240 pallet::Attempt::<T>::insert(
241 &lost,
242 0u32, (attempt, ticket, held_deposit),
244 );
245 },
246 MigrationCursor::Proxy(last_key) => {
247 let mut iter = if let Some(ref key) = last_key {
248 v0::Proxy::<T>::iter_from_key(key)
249 } else {
250 v0::Proxy::<T>::iter()
251 };
252
253 let Some((rescuer, lost)) = iter.next() else {
254 StorageVersion::new(Self::id().version_to as u16).put::<Pallet<T>>();
256 return Ok(None);
257 };
258 cursor = MigrationCursor::Proxy(Some(rescuer.clone()));
259 v0::Proxy::<T>::remove(&rescuer);
260
261 let _ = frame_system::Pallet::<T>::dec_consumers(&rescuer);
263
264 let inheritor = rescuer.clone();
265 let inheritance_priority = 0u32;
266
267 let ticket = match Pallet::<T>::inheritor_ticket(&inheritor) {
269 Ok(ticket) => ticket,
270 Err(e) => {
271 log::error!("MigrateV0ToV1: Failed to create Inheritor ticket for rescuer {:?}: {:?}", inheritor, e);
272 crate::IdentifiedConsideration {
273 depositor: rescuer.clone(),
274 ticket: None,
275 _phantom: Default::default(),
276 }
277 },
278 };
279
280 pallet::Inheritor::<T>::insert(
281 &lost,
282 (inheritance_priority, inheritor, ticket),
283 );
284 },
285 }
286 }
287
288 Ok(Some(cursor))
289 }
290
291 #[cfg(feature = "try-runtime")]
292 fn pre_upgrade() -> Result<Vec<u8>, frame_support::sp_runtime::TryRuntimeError> {
293 use codec::Encode;
294
295 let recoverable_count = v0::Recoverable::<T>::iter().count() as u32;
296 let active_recoveries_count = v0::ActiveRecoveries::<T>::iter().count() as u32;
297 let proxy_count = v0::Proxy::<T>::iter().count() as u32;
298
299 log::info!(
300 target: "runtime::recovery",
301 "MigrateV0ToV1: pre_upgrade - Recoverable: {}, ActiveRecoveries: {}, Proxy: {}",
302 recoverable_count,
303 active_recoveries_count,
304 proxy_count,
305 );
306
307 Ok((recoverable_count, active_recoveries_count, proxy_count).encode())
308 }
309
310 #[cfg(feature = "try-runtime")]
311 fn post_upgrade(state: Vec<u8>) -> Result<(), frame_support::sp_runtime::TryRuntimeError> {
312 use codec::Decode;
313
314 let (recoverable_count, active_recoveries_count, proxy_count) =
315 <(u32, u32, u32)>::decode(&mut &state[..]).expect("Failed to decode pre_upgrade state");
316
317 assert_eq!(v0::Recoverable::<T>::iter().count(), 0);
319 assert_eq!(v0::ActiveRecoveries::<T>::iter().count(), 0);
320 assert_eq!(v0::Proxy::<T>::iter().count(), 0);
321
322 let friend_groups_count = pallet::FriendGroups::<T>::iter().count() as u32;
324 let attempt_count = pallet::Attempt::<T>::iter().count() as u32;
325 let inheritor_count = pallet::Inheritor::<T>::iter().count() as u32;
326
327 log::info!(
328 target: "runtime::recovery",
329 "MigrateV0ToV1: post_upgrade - FriendGroups: {}, Attempt: {}, Inheritor: {}",
330 friend_groups_count,
331 attempt_count,
332 inheritor_count,
333 );
334
335 if friend_groups_count != recoverable_count {
337 log::error!(
338 "MigrateV0ToV1: FriendGroups count mismatch: {} != {}",
339 friend_groups_count,
340 recoverable_count
341 );
342 }
343 if attempt_count != active_recoveries_count {
344 log::error!(
345 "MigrateV0ToV1: Attempt count mismatch: {} != {}",
346 attempt_count,
347 active_recoveries_count
348 );
349 }
350 if inheritor_count != proxy_count {
351 log::error!(
352 "MigrateV0ToV1: Inheritor count mismatch: {} != {}",
353 inheritor_count,
354 proxy_count
355 );
356 }
357
358 Ok(())
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::{v0, MigrateV0ToV1};
365 use crate::{
366 mock::{new_test_ext, Balances, Test, ALICE, BOB, CHARLIE, DAVE, EVE},
367 pallet,
368 };
369 use frame_support::{
370 migrations::SteppedMigration,
371 traits::{GetStorageVersion, ReservableCurrency, StorageVersion},
372 weights::WeightMeter,
373 BoundedVec,
374 };
375
376 type T = Test;
377
378 fn friends(accounts: &[u64]) -> v0::FriendsOf<T> {
379 let mut f: Vec<u64> = accounts.to_vec();
380 f.sort();
381 BoundedVec::try_from(f).unwrap()
382 }
383
384 fn run_migration() {
385 let mut cursor = None;
386
387 #[cfg(feature = "try-runtime")]
388 let data = MigrateV0ToV1::<T>::pre_upgrade().unwrap();
389
390 loop {
391 let mut meter = WeightMeter::new();
392 match MigrateV0ToV1::<T>::step(cursor, &mut meter) {
393 Ok(None) => break,
394 Ok(Some(c)) => cursor = Some(c),
395 Err(e) => panic!("Migration failed: {:?}", e),
396 }
397 }
398
399 #[cfg(feature = "try-runtime")]
400 MigrateV0ToV1::<T>::post_upgrade(data).unwrap();
401 }
402
403 #[test]
404 fn migration_works() {
405 new_test_ext().execute_with(|| {
406 let config_deposit = 50u128;
407 let recovery_deposit = 100u128;
408
409 v0::Recoverable::<T>::insert(
413 ALICE,
414 v0::RecoveryConfig {
415 delay_period: 10u64,
416 deposit: config_deposit,
417 friends: friends(&[BOB, CHARLIE]),
418 threshold: 2,
419 },
420 );
421 Balances::reserve(&ALICE, config_deposit).unwrap();
422
423 v0::Recoverable::<T>::insert(
424 BOB,
425 v0::RecoveryConfig {
426 delay_period: 5u64,
427 deposit: config_deposit,
428 friends: friends(&[ALICE, CHARLIE]),
429 threshold: 1,
430 },
431 );
432 Balances::reserve(&BOB, config_deposit).unwrap();
433
434 v0::Recoverable::<T>::insert(
436 EVE,
437 v0::RecoveryConfig {
438 delay_period: 0u64,
439 deposit: config_deposit,
440 friends: friends(&[ALICE, BOB]),
441 threshold: 1,
442 },
443 );
444 Balances::reserve(&EVE, config_deposit).unwrap();
445
446 v0::ActiveRecoveries::<T>::insert(
448 ALICE,
449 CHARLIE,
450 v0::ActiveRecovery {
451 created: 1u64,
452 deposit: recovery_deposit,
453 friends: friends(&[BOB]),
454 },
455 );
456 Balances::reserve(&CHARLIE, recovery_deposit).unwrap();
457
458 v0::Proxy::<T>::insert(DAVE, EVE);
460 frame_system::Pallet::<T>::inc_consumers(&DAVE).unwrap();
461
462 assert_eq!(v0::Recoverable::<T>::iter().count(), 3);
464 assert_eq!(v0::ActiveRecoveries::<T>::iter().count(), 1);
465 assert_eq!(v0::Proxy::<T>::iter().count(), 1);
466 assert_eq!(Balances::reserved_balance(ALICE), config_deposit);
467 assert_eq!(Balances::reserved_balance(BOB), config_deposit);
468 assert_eq!(Balances::reserved_balance(CHARLIE), recovery_deposit);
469 assert_eq!(Balances::reserved_balance(EVE), config_deposit);
470 assert_eq!(frame_system::Pallet::<T>::consumers(&DAVE), 1);
471
472 run_migration();
474
475 assert_eq!(v0::Recoverable::<T>::iter().count(), 0);
477 assert_eq!(v0::ActiveRecoveries::<T>::iter().count(), 0);
478 assert_eq!(v0::Proxy::<T>::iter().count(), 0);
479
480 assert_eq!(pallet::FriendGroups::<T>::iter().count(), 3);
484
485 let (alice_groups, _ticket) = pallet::FriendGroups::<T>::get(ALICE).unwrap();
487 assert_eq!(alice_groups.len(), 1);
488 let alice_fg = &alice_groups[0];
489 assert_eq!(alice_fg.friends.len(), 2);
490 assert!(alice_fg.friends.contains(&BOB));
491 assert!(alice_fg.friends.contains(&CHARLIE));
492 assert_eq!(alice_fg.friends_needed, 2);
493 assert_eq!(alice_fg.inheritance_delay, 10);
494 let expected_inheritor = v0::multi_account_id::<u64>(&[BOB, CHARLIE], 2);
496 assert_eq!(alice_fg.inheritor, expected_inheritor);
497 assert_eq!(alice_fg.inheritance_priority, 0);
498
499 let (bob_groups, _ticket) = pallet::FriendGroups::<T>::get(BOB).unwrap();
501 assert_eq!(bob_groups.len(), 1);
502 let bob_fg = &bob_groups[0];
503 assert_eq!(bob_fg.friends_needed, 1);
504 assert_eq!(bob_fg.inheritance_delay, 5);
505
506 let (eve_groups, _ticket) = pallet::FriendGroups::<T>::get(EVE).unwrap();
508 assert_eq!(eve_groups.len(), 1);
509 let eve_fg = &eve_groups[0];
510 assert_eq!(eve_fg.inheritance_delay, 0);
511 assert_eq!(eve_fg.cancel_delay, 1);
512
513 assert_eq!(pallet::Inheritor::<T>::iter().count(), 1);
515 let (order, inheritor, _ticket) = pallet::Inheritor::<T>::get(EVE).unwrap();
516 assert_eq!(inheritor, DAVE);
517 assert_eq!(order, 0);
518
519 assert_eq!(pallet::Attempt::<T>::iter().count(), 1);
522 let (attempt, _ticket, deposit) = pallet::Attempt::<T>::get(ALICE, 0u32).unwrap();
523 assert_eq!(attempt.initiator, CHARLIE);
524 assert_eq!(attempt.friend_group_index, 0);
525 assert_eq!(deposit, crate::mock::SECURITY_DEPOSIT);
526 });
527 }
528
529 #[test]
530 fn migration_inserts_attempt_when_security_deposit_hold_fails() {
531 new_test_ext().execute_with(|| {
532 let config_deposit = 50u128;
533 let old_recovery_deposit = 10u128;
535
536 let rescuer: u64 = 99;
540 let lost = ALICE;
541
542 let rescuer_free = 50u128;
546 pallet_balances::Pallet::<Test>::force_set_balance(
547 frame_system::RawOrigin::Root.into(),
548 rescuer,
549 rescuer_free + old_recovery_deposit,
550 )
551 .unwrap();
552 Balances::reserve(&rescuer, old_recovery_deposit).unwrap();
553
554 v0::Recoverable::<T>::insert(
557 lost,
558 v0::RecoveryConfig {
559 delay_period: 10u64,
560 deposit: config_deposit,
561 friends: friends(&[BOB, CHARLIE]),
562 threshold: 2,
563 },
564 );
565 Balances::reserve(&lost, config_deposit).unwrap();
566
567 v0::ActiveRecoveries::<T>::insert(
569 lost,
570 rescuer,
571 v0::ActiveRecovery {
572 created: 1u64,
573 deposit: old_recovery_deposit,
574 friends: BoundedVec::default(), },
576 );
577
578 assert_eq!(v0::ActiveRecoveries::<T>::iter().count(), 1);
579
580 run_migration();
582
583 assert_eq!(v0::ActiveRecoveries::<T>::iter().count(), 0);
585
586 assert_eq!(
587 pallet::Attempt::<T>::iter().count(),
588 1,
589 "Attempt entry was not inserted during migration — active recovery lost!"
590 );
591 });
592 }
593
594 #[test]
595 fn migrated_recovery_can_be_completed() {
596 use crate::mock::{signed, Recovery};
597 use frame_support::assert_ok;
598
599 new_test_ext().execute_with(|| {
600 let config_deposit = 50u128;
601 let recovery_deposit = 100u128;
602
603 v0::Recoverable::<T>::insert(
606 ALICE,
607 v0::RecoveryConfig {
608 delay_period: 10u64,
609 deposit: config_deposit,
610 friends: friends(&[BOB, CHARLIE, DAVE]),
611 threshold: 2,
612 },
613 );
614 Balances::reserve(&ALICE, config_deposit).unwrap();
615
616 v0::ActiveRecoveries::<T>::insert(
618 ALICE,
619 BOB,
620 v0::ActiveRecovery {
621 created: 1u64,
622 deposit: recovery_deposit,
623 friends: friends(&[CHARLIE]), },
625 );
626 Balances::reserve(&BOB, recovery_deposit).unwrap();
627
628 run_migration();
630
631 assert_eq!(pallet::FriendGroups::<T>::iter().count(), 1);
633 assert_eq!(pallet::Attempt::<T>::iter().count(), 1);
634
635 let multisig_inheritor = v0::multi_account_id::<u64>(&[BOB, CHARLIE, DAVE], 2);
637 let (groups, _) = pallet::FriendGroups::<T>::get(ALICE).unwrap();
639 assert_eq!(groups[0].inheritor, multisig_inheritor);
640
641 let (attempt, _, _) = pallet::Attempt::<T>::get(ALICE, 0u32).unwrap();
643 assert_eq!(attempt.initiator, BOB);
644 assert_eq!(attempt.approvals.count_ones(), 1);
646
647 assert_ok!(Recovery::approve_attempt(signed(DAVE), ALICE, 0));
651
652 let (attempt, _, _) = pallet::Attempt::<T>::get(ALICE, 0u32).unwrap();
654 assert_eq!(attempt.approvals.count_ones(), 2);
655
656 frame_system::Pallet::<T>::set_block_number(15);
658
659 assert_ok!(Recovery::finish_attempt(signed(BOB), ALICE, 0));
661
662 let (order, inheritor, _) = pallet::Inheritor::<T>::get(ALICE).unwrap();
664 assert_eq!(inheritor, multisig_inheritor);
665 assert_eq!(order, 0);
666
667 assert!(pallet::Attempt::<T>::get(ALICE, 0u32).is_none());
669 });
670 }
671
672 #[test]
673 fn migration_bumps_on_chain_storage_version() {
674 new_test_ext().execute_with(|| {
675 StorageVersion::new(0).put::<pallet::Pallet<T>>();
676 assert_eq!(pallet::Pallet::<T>::on_chain_storage_version(), 0);
677
678 v0::Recoverable::<T>::insert(
679 ALICE,
680 v0::RecoveryConfig {
681 delay_period: 10u64,
682 deposit: 50u128,
683 friends: friends(&[BOB, CHARLIE]),
684 threshold: 2,
685 },
686 );
687 Balances::reserve(&ALICE, 50u128).unwrap();
688
689 run_migration();
690
691 assert_eq!(pallet::Pallet::<T>::on_chain_storage_version(), 1);
692 });
693 }
694
695 #[test]
696 fn migration_is_idempotent_after_completion() {
697 new_test_ext().execute_with(|| {
698 StorageVersion::new(0).put::<pallet::Pallet<T>>();
699
700 v0::Recoverable::<T>::insert(
701 ALICE,
702 v0::RecoveryConfig {
703 delay_period: 10u64,
704 deposit: 50u128,
705 friends: friends(&[BOB, CHARLIE]),
706 threshold: 2,
707 },
708 );
709 Balances::reserve(&ALICE, 50u128).unwrap();
710
711 run_migration();
712 assert_eq!(pallet::Pallet::<T>::on_chain_storage_version(), 1);
713
714 let _guard = frame_support::StorageNoopGuard::new();
715 let mut meter = WeightMeter::new();
716 assert!(matches!(MigrateV0ToV1::<T>::step(None, &mut meter), Ok(None)));
717 });
718 }
719
720 #[test]
721 fn migration_inserts_attempt_when_storage_ticket_fails() {
722 new_test_ext().execute_with(|| {
723 let config_deposit = 50u128;
724 let old_recovery_deposit = 10u128;
725 let rescuer: u64 = 99;
726 let lost = ALICE;
727
728 pallet_balances::Pallet::<Test>::force_set_balance(
729 frame_system::RawOrigin::Root.into(),
730 rescuer,
731 crate::mock::ExistentialDeposit::get() as u128 + old_recovery_deposit,
732 )
733 .unwrap();
734 Balances::reserve(&rescuer, old_recovery_deposit).unwrap();
735
736 v0::Recoverable::<T>::insert(
737 lost,
738 v0::RecoveryConfig {
739 delay_period: 10u64,
740 deposit: config_deposit,
741 friends: friends(&[BOB, CHARLIE]),
742 threshold: 2,
743 },
744 );
745 Balances::reserve(&lost, config_deposit).unwrap();
746
747 v0::ActiveRecoveries::<T>::insert(
748 lost,
749 rescuer,
750 v0::ActiveRecovery {
751 created: 1u64,
752 deposit: old_recovery_deposit,
753 friends: BoundedVec::default(),
754 },
755 );
756
757 run_migration();
758
759 assert_eq!(v0::ActiveRecoveries::<T>::iter().count(), 0);
760 assert_eq!(
761 pallet::Attempt::<T>::iter().count(),
762 1,
763 "Attempt entry must survive even when storage ticket creation fails",
764 );
765
766 let (attempt, ticket, held_deposit) = pallet::Attempt::<T>::get(lost, 0u32).unwrap();
767 assert_eq!(attempt.initiator, rescuer);
768 assert!(ticket.ticket.is_none(), "Inner ticket must be None when storage hold failed");
769 assert_eq!(ticket.depositor, rescuer);
770 assert_eq!(held_deposit, 0, "Security deposit must be zero when hold failed");
771
772 use frame::traits::fungible::InspectHold;
773 assert_eq!(
774 Balances::balance_on_hold(&crate::HoldReason::AttemptStorage.into(), &rescuer),
775 0,
776 );
777 assert_eq!(
778 Balances::balance_on_hold(&crate::HoldReason::SecurityDeposit.into(), &rescuer),
779 0,
780 );
781 });
782 }
783
784 #[test]
785 fn migration_inserts_inheritor_when_ticket_fails() {
786 use frame::traits::fungible::InspectHold;
787
788 new_test_ext().execute_with(|| {
789 let rescuer: u64 = 99;
790 let lost = ALICE;
791
792 pallet_balances::Pallet::<Test>::force_set_balance(
793 frame_system::RawOrigin::Root.into(),
794 rescuer,
795 crate::mock::ExistentialDeposit::get() as u128,
796 )
797 .unwrap();
798 frame_system::Pallet::<T>::inc_consumers(&rescuer).unwrap();
799
800 v0::Proxy::<T>::insert(rescuer, lost);
801 assert_eq!(frame_system::Pallet::<T>::consumers(&rescuer), 1);
802
803 run_migration();
804
805 assert_eq!(v0::Proxy::<T>::iter().count(), 0);
806 assert_eq!(frame_system::Pallet::<T>::consumers(&rescuer), 0);
807
808 assert_eq!(
809 pallet::Inheritor::<T>::iter().count(),
810 1,
811 "Inheritor entry must survive even when ticket creation fails",
812 );
813 let (priority, inheritor, ticket) = pallet::Inheritor::<T>::get(lost).unwrap();
814 assert_eq!(inheritor, rescuer);
815 assert_eq!(priority, 0);
816 assert!(ticket.ticket.is_none(), "Inner ticket must be None when hold failed");
817 assert_eq!(ticket.depositor, rescuer);
818
819 assert_eq!(
820 Balances::balance_on_hold(&crate::HoldReason::InheritorStorage.into(), &rescuer),
821 0,
822 );
823 });
824 }
825}