1
// Copyright 2019-2025 PureStake Inc.
2
// This file is part of Moonbeam.
3

            
4
// Moonbeam is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Moonbeam is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16

            
17
//! Scheduled requests functionality for delegators
18

            
19
use crate::pallet::{
20
	BalanceOf, CandidateInfo, Config, DelegationScheduledRequests,
21
	DelegationScheduledRequestsPerCollator, DelegatorState, Error, Event, Pallet, Round,
22
	RoundIndex, Total,
23
};
24
use crate::weights::WeightInfo;
25
use crate::{auto_compound::AutoCompoundDelegations, Delegator};
26
use frame_support::dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo};
27
use frame_support::ensure;
28
use frame_support::traits::Get;
29
use frame_support::BoundedVec;
30
use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
31
use scale_info::TypeInfo;
32
use sp_runtime::{
33
	traits::{Saturating, Zero},
34
	RuntimeDebug,
35
};
36

            
37
/// An action that can be performed upon a delegation
38
#[derive(
39
	Clone,
40
	Eq,
41
	PartialEq,
42
	Encode,
43
	Decode,
44
	RuntimeDebug,
45
	TypeInfo,
46
	PartialOrd,
47
	Ord,
48
	DecodeWithMemTracking,
49
	MaxEncodedLen,
50
)]
51
pub enum DelegationAction<Balance> {
52
	Revoke(Balance),
53
	Decrease(Balance),
54
}
55

            
56
impl<Balance: Copy> DelegationAction<Balance> {
57
	/// Returns the wrapped amount value.
58
96
	pub fn amount(&self) -> Balance {
59
96
		match self {
60
33
			DelegationAction::Revoke(amount) => *amount,
61
63
			DelegationAction::Decrease(amount) => *amount,
62
		}
63
96
	}
64
}
65

            
66
/// Represents a scheduled request that defines a [`DelegationAction`]. The request is executable
67
/// iff the provided [`RoundIndex`] is achieved.
68
#[derive(
69
	Clone,
70
	Eq,
71
	PartialEq,
72
	Encode,
73
	Decode,
74
	RuntimeDebug,
75
	TypeInfo,
76
	PartialOrd,
77
	Ord,
78
	DecodeWithMemTracking,
79
	MaxEncodedLen,
80
)]
81
pub struct ScheduledRequest<Balance> {
82
	pub when_executable: RoundIndex,
83
	pub action: DelegationAction<Balance>,
84
}
85

            
86
/// Represents a cancelled scheduled request for emitting an event.
87
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, DecodeWithMemTracking)]
88
pub struct CancelledScheduledRequest<Balance> {
89
	pub when_executable: RoundIndex,
90
	pub action: DelegationAction<Balance>,
91
}
92

            
93
impl<B> From<ScheduledRequest<B>> for CancelledScheduledRequest<B> {
94
10
	fn from(request: ScheduledRequest<B>) -> Self {
95
10
		CancelledScheduledRequest {
96
10
			when_executable: request.when_executable,
97
10
			action: request.action,
98
10
		}
99
10
	}
100
}
101

            
102
impl<T: Config> Pallet<T> {
103
	/// Schedules a [DelegationAction::Revoke] for the delegator, towards a given collator.
104
52
	pub(crate) fn delegation_schedule_revoke(
105
52
		collator: T::AccountId,
106
52
		delegator: T::AccountId,
107
52
	) -> DispatchResultWithPostInfo {
108
52
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
109
50
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
110

            
111
50
		let actual_weight =
112
50
			<T as Config>::WeightInfo::schedule_revoke_delegation(scheduled_requests.len() as u32);
113

            
114
50
		let is_new_delegator = scheduled_requests.is_empty();
115

            
116
50
		ensure!(
117
50
			is_new_delegator,
118
1
			DispatchErrorWithPostInfo {
119
1
				post_info: Some(actual_weight).into(),
120
1
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
121
1
			},
122
		);
123

            
124
		// This is the first scheduled request for this delegator towards this collator,
125
		// ensure we do not exceed the maximum number of delegators that can have pending
126
		// requests for the collator.
127
49
		let current = <DelegationScheduledRequestsPerCollator<T>>::get(&collator);
128
49
		if current >= Pallet::<T>::max_delegators_per_candidate() {
129
			return Err(DispatchErrorWithPostInfo {
130
				post_info: Some(actual_weight).into(),
131
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
132
			});
133
49
		}
134

            
135
49
		let bonded_amount = state
136
49
			.get_bond_amount(&collator)
137
49
			.ok_or(<Error<T>>::DelegationDNE)?;
138
48
		let now = <Round<T>>::get().current;
139
48
		let when = now.saturating_add(T::RevokeDelegationDelay::get());
140
48
		scheduled_requests
141
48
			.try_push(ScheduledRequest {
142
48
				action: DelegationAction::Revoke(bonded_amount),
143
48
				when_executable: when,
144
48
			})
145
48
			.map_err(|_| DispatchErrorWithPostInfo {
146
				post_info: Some(actual_weight).into(),
147
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
148
			})?;
149
48
		state.less_total = state.less_total.saturating_add(bonded_amount);
150

            
151
48
		<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
152
48
			*c = c.saturating_add(1);
153
48
		});
154

            
155
48
		<DelegationScheduledRequests<T>>::insert(
156
48
			collator.clone(),
157
48
			delegator.clone(),
158
48
			scheduled_requests,
159
		);
160
48
		<DelegatorState<T>>::insert(delegator.clone(), state);
161

            
162
48
		Self::deposit_event(Event::DelegationRevocationScheduled {
163
48
			round: now,
164
48
			delegator,
165
48
			candidate: collator,
166
48
			scheduled_exit: when,
167
48
		});
168
48
		Ok(().into())
169
52
	}
170

            
171
	/// Schedules a [DelegationAction::Decrease] for the delegator, towards a given collator.
172
89
	pub(crate) fn delegation_schedule_bond_decrease(
173
89
		collator: T::AccountId,
174
89
		delegator: T::AccountId,
175
89
		decrease_amount: BalanceOf<T>,
176
89
	) -> DispatchResultWithPostInfo {
177
89
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
178
88
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
179

            
180
88
		let actual_weight = <T as Config>::WeightInfo::schedule_delegator_bond_less(
181
88
			scheduled_requests.len() as u32,
182
		);
183

            
184
		// If this is the first scheduled request for this delegator towards this collator,
185
		// ensure we do not exceed the maximum number of delegators that can have pending
186
		// requests for the collator.
187
88
		let is_new_delegator = scheduled_requests.is_empty();
188
88
		if is_new_delegator {
189
33
			let current = <DelegationScheduledRequestsPerCollator<T>>::get(&collator);
190
33
			let max_delegators = Pallet::<T>::max_delegators_per_candidate();
191
33
			if current >= max_delegators {
192
1
				return Err(DispatchErrorWithPostInfo {
193
1
					post_info: Some(actual_weight).into(),
194
1
					error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
195
1
				});
196
32
			}
197
55
		}
198

            
199
87
		ensure!(
200
87
			!scheduled_requests
201
87
				.iter()
202
1284
				.any(|req| matches!(req.action, DelegationAction::Revoke(_))),
203
2
			DispatchErrorWithPostInfo {
204
2
				post_info: Some(actual_weight).into(),
205
2
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
206
2
			},
207
		);
208

            
209
85
		let bonded_amount = state
210
85
			.get_bond_amount(&collator)
211
85
			.ok_or(DispatchErrorWithPostInfo {
212
85
				post_info: Some(actual_weight).into(),
213
85
				error: <Error<T>>::DelegationDNE.into(),
214
85
			})?;
215
83
		ensure!(
216
83
			bonded_amount > decrease_amount,
217
1
			DispatchErrorWithPostInfo {
218
1
				post_info: Some(actual_weight).into(),
219
1
				error: <Error<T>>::DelegatorBondBelowMin.into(),
220
1
			},
221
		);
222
82
		let new_amount: BalanceOf<T> = (bonded_amount - decrease_amount).into();
223
82
		ensure!(
224
82
			new_amount >= T::MinDelegation::get(),
225
1
			DispatchErrorWithPostInfo {
226
1
				post_info: Some(actual_weight).into(),
227
1
				error: <Error<T>>::DelegationBelowMin.into(),
228
1
			},
229
		);
230

            
231
		// Net Total is total after pending orders are executed
232
81
		let net_total = state.total().saturating_sub(state.less_total);
233
		// Net Total is always >= MinDelegation
234
81
		let max_subtracted_amount = net_total.saturating_sub(T::MinDelegation::get().into());
235
81
		ensure!(
236
81
			decrease_amount <= max_subtracted_amount,
237
			DispatchErrorWithPostInfo {
238
				post_info: Some(actual_weight).into(),
239
				error: <Error<T>>::DelegatorBondBelowMin.into(),
240
			},
241
		);
242

            
243
81
		let now = <Round<T>>::get().current;
244
81
		let when = now.saturating_add(T::DelegationBondLessDelay::get());
245
81
		scheduled_requests
246
81
			.try_push(ScheduledRequest {
247
81
				action: DelegationAction::Decrease(decrease_amount),
248
81
				when_executable: when,
249
81
			})
250
81
			.map_err(|_| DispatchErrorWithPostInfo {
251
1
				post_info: Some(actual_weight).into(),
252
1
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
253
1
			})?;
254
80
		state.less_total = state.less_total.saturating_add(decrease_amount);
255
80
		if is_new_delegator {
256
28
			<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
257
28
				*c = c.saturating_add(1);
258
28
			});
259
52
		}
260
80
		<DelegationScheduledRequests<T>>::insert(
261
80
			collator.clone(),
262
80
			delegator.clone(),
263
80
			scheduled_requests,
264
		);
265
80
		<DelegatorState<T>>::insert(delegator.clone(), state);
266

            
267
80
		Self::deposit_event(Event::DelegationDecreaseScheduled {
268
80
			delegator,
269
80
			candidate: collator,
270
80
			amount_to_decrease: decrease_amount,
271
80
			execute_round: when,
272
80
		});
273
80
		Ok(Some(actual_weight).into())
274
89
	}
275

            
276
	/// Cancels the delegator's existing [ScheduledRequest] towards a given collator.
277
10
	pub(crate) fn delegation_cancel_request(
278
10
		collator: T::AccountId,
279
10
		delegator: T::AccountId,
280
10
	) -> DispatchResultWithPostInfo {
281
10
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
282
10
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
283
10
		let actual_weight =
284
10
			<T as Config>::WeightInfo::cancel_delegation_request(scheduled_requests.len() as u32);
285

            
286
10
		let request = Self::cancel_request_with_state(&mut state, &mut scheduled_requests).ok_or(
287
10
			DispatchErrorWithPostInfo {
288
10
				post_info: Some(actual_weight).into(),
289
10
				error: <Error<T>>::PendingDelegationRequestDNE.into(),
290
10
			},
291
		)?;
292

            
293
10
		if scheduled_requests.is_empty() {
294
10
			<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
295
10
				*c = c.saturating_sub(1);
296
10
			});
297
10
			<DelegationScheduledRequests<T>>::remove(&collator, &delegator);
298
		} else {
299
			<DelegationScheduledRequests<T>>::insert(
300
				collator.clone(),
301
				delegator.clone(),
302
				scheduled_requests,
303
			);
304
		}
305
10
		<DelegatorState<T>>::insert(delegator.clone(), state);
306

            
307
10
		Self::deposit_event(Event::CancelledDelegationRequest {
308
10
			delegator,
309
10
			collator,
310
10
			cancelled_request: request.into(),
311
10
		});
312
10
		Ok(Some(actual_weight).into())
313
10
	}
314

            
315
12
	fn cancel_request_with_state(
316
12
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
317
12
		scheduled_requests: &mut BoundedVec<
318
12
			ScheduledRequest<BalanceOf<T>>,
319
12
			T::MaxScheduledRequestsPerDelegator,
320
12
		>,
321
12
	) -> Option<ScheduledRequest<BalanceOf<T>>> {
322
12
		if scheduled_requests.is_empty() {
323
1
			return None;
324
11
		}
325

            
326
		// `BoundedVec::remove` can panic, but we make sure it will not happen by
327
		// checking above that `scheduled_requests` is not empty.
328
11
		let request = scheduled_requests.remove(0);
329
11
		let amount = request.action.amount();
330
11
		state.less_total = state.less_total.saturating_sub(amount);
331
11
		Some(request)
332
12
	}
333

            
334
	/// Executes the delegator's existing [ScheduledRequest] towards a given collator.
335
39
	pub(crate) fn delegation_execute_scheduled_request(
336
39
		collator: T::AccountId,
337
39
		delegator: T::AccountId,
338
39
	) -> DispatchResultWithPostInfo {
339
39
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
340
39
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
341
39
		let request = scheduled_requests
342
39
			.first()
343
39
			.ok_or(<Error<T>>::PendingDelegationRequestDNE)?;
344

            
345
39
		let now = <Round<T>>::get().current;
346
39
		ensure!(
347
39
			request.when_executable <= now,
348
			<Error<T>>::PendingDelegationRequestNotDueYet
349
		);
350

            
351
39
		match request.action {
352
23
			DelegationAction::Revoke(amount) => {
353
23
				let actual_weight =
354
23
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
355

            
356
				// revoking last delegation => leaving set of delegators
357
23
				let leaving = if state.delegations.0.len() == 1usize {
358
12
					true
359
				} else {
360
11
					ensure!(
361
11
						state.total().saturating_sub(T::MinDelegation::get().into()) >= amount,
362
						DispatchErrorWithPostInfo {
363
							post_info: Some(actual_weight).into(),
364
							error: <Error<T>>::DelegatorBondBelowMin.into(),
365
						}
366
					);
367
11
					false
368
				};
369

            
370
				// remove from pending requests
371
				// `BoundedVec::remove` can panic, but we make sure it will not happen by checking above that `scheduled_requests` is not empty.
372
23
				let amount = scheduled_requests.remove(0).action.amount();
373
23
				state.less_total = state.less_total.saturating_sub(amount);
374

            
375
				// remove delegation from delegator state
376
23
				state.rm_delegation::<T>(&collator);
377

            
378
				// remove delegation from auto-compounding info
379
23
				<AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
380

            
381
				// remove delegation from collator state delegations
382
23
				Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), amount)
383
23
					.map_err(|err| DispatchErrorWithPostInfo {
384
						post_info: Some(actual_weight).into(),
385
						error: err,
386
					})?;
387
23
				Self::deposit_event(Event::DelegationRevoked {
388
23
					delegator: delegator.clone(),
389
23
					candidate: collator.clone(),
390
23
					unstaked_amount: amount,
391
23
				});
392
23
				if scheduled_requests.is_empty() {
393
23
					<DelegationScheduledRequests<T>>::remove(&collator, &delegator);
394
23
					<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
395
23
						*c = c.saturating_sub(1);
396
23
					});
397
				} else {
398
					<DelegationScheduledRequests<T>>::insert(
399
						collator.clone(),
400
						delegator.clone(),
401
						scheduled_requests,
402
					);
403
				}
404
23
				if leaving {
405
12
					<DelegatorState<T>>::remove(&delegator);
406
12
					Self::deposit_event(Event::DelegatorLeft {
407
12
						delegator,
408
12
						unstaked_amount: amount,
409
12
					});
410
12
				} else {
411
11
					<DelegatorState<T>>::insert(&delegator, state);
412
11
				}
413
23
				Ok(Some(actual_weight).into())
414
			}
415
			DelegationAction::Decrease(_) => {
416
16
				let actual_weight =
417
16
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
418

            
419
				// remove from pending requests
420
				// `BoundedVec::remove` can panic, but we make sure it will not happen by checking above that `scheduled_requests` is not empty.
421
16
				let amount = scheduled_requests.remove(0).action.amount();
422
16
				state.less_total = state.less_total.saturating_sub(amount);
423

            
424
				// decrease delegation
425
16
				for bond in &mut state.delegations.0 {
426
16
					if bond.owner == collator {
427
16
						return if bond.amount > amount {
428
16
							let amount_before: BalanceOf<T> = bond.amount.into();
429
16
							bond.amount = bond.amount.saturating_sub(amount);
430
16
							let mut collator_info = <CandidateInfo<T>>::get(&collator)
431
16
								.ok_or(<Error<T>>::CandidateDNE)
432
16
								.map_err(|err| DispatchErrorWithPostInfo {
433
									post_info: Some(actual_weight).into(),
434
									error: err.into(),
435
								})?;
436

            
437
16
							state
438
16
								.total_sub_if::<T, _>(amount, |total| {
439
16
									let new_total: BalanceOf<T> = total.into();
440
16
									ensure!(
441
16
										new_total >= T::MinDelegation::get(),
442
										<Error<T>>::DelegationBelowMin
443
									);
444

            
445
16
									Ok(())
446
16
								})
447
16
								.map_err(|err| DispatchErrorWithPostInfo {
448
									post_info: Some(actual_weight).into(),
449
									error: err,
450
								})?;
451

            
452
							// need to go into decrease_delegation
453
16
							let in_top = collator_info
454
16
								.decrease_delegation::<T>(
455
16
									&collator,
456
16
									delegator.clone(),
457
16
									amount_before,
458
16
									amount,
459
								)
460
16
								.map_err(|err| DispatchErrorWithPostInfo {
461
									post_info: Some(actual_weight).into(),
462
									error: err,
463
								})?;
464
16
							<CandidateInfo<T>>::insert(&collator, collator_info);
465
16
							let new_total_staked = <Total<T>>::get().saturating_sub(amount);
466
16
							<Total<T>>::put(new_total_staked);
467

            
468
16
							if scheduled_requests.is_empty() {
469
15
								<DelegationScheduledRequests<T>>::remove(&collator, &delegator);
470
15
								<DelegationScheduledRequestsPerCollator<T>>::mutate(
471
15
									&collator,
472
15
									|c| {
473
15
										*c = c.saturating_sub(1);
474
15
									},
475
								);
476
1
							} else {
477
1
								<DelegationScheduledRequests<T>>::insert(
478
1
									collator.clone(),
479
1
									delegator.clone(),
480
1
									scheduled_requests,
481
1
								);
482
1
							}
483
16
							<DelegatorState<T>>::insert(delegator.clone(), state);
484
16
							Self::deposit_event(Event::DelegationDecreased {
485
16
								delegator,
486
16
								candidate: collator.clone(),
487
16
								amount,
488
16
								in_top,
489
16
							});
490
16
							Ok(Some(actual_weight).into())
491
						} else {
492
							// must rm entire delegation if bond.amount <= less or cancel request
493
							Err(DispatchErrorWithPostInfo {
494
								post_info: Some(actual_weight).into(),
495
								error: <Error<T>>::DelegationBelowMin.into(),
496
							})
497
						};
498
					}
499
				}
500
				Err(DispatchErrorWithPostInfo {
501
					post_info: Some(actual_weight).into(),
502
					error: <Error<T>>::DelegationDNE.into(),
503
				})
504
			}
505
		}
506
39
	}
507

            
508
	/// Removes the delegator's existing [ScheduledRequest] towards a given collator, if exists.
509
	/// The state needs to be persisted by the caller of this function.
510
19
	pub(crate) fn delegation_remove_request_with_state(
511
19
		collator: &T::AccountId,
512
19
		delegator: &T::AccountId,
513
19
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
514
19
	) {
515
19
		let scheduled_requests = <DelegationScheduledRequests<T>>::get(collator, delegator);
516

            
517
19
		if scheduled_requests.is_empty() {
518
15
			return;
519
4
		}
520

            
521
		// Calculate total amount across all scheduled requests
522
4
		let total_amount: BalanceOf<T> = scheduled_requests
523
4
			.iter()
524
4
			.map(|request| request.action.amount())
525
4
			.fold(BalanceOf::<T>::zero(), |acc, amount| {
526
4
				acc.saturating_add(amount)
527
4
			});
528

            
529
4
		state.less_total = state.less_total.saturating_sub(total_amount);
530
4
		<DelegationScheduledRequests<T>>::remove(collator, delegator);
531
4
		<DelegationScheduledRequestsPerCollator<T>>::mutate(collator, |c| {
532
4
			*c = c.saturating_sub(1);
533
4
		});
534
19
	}
535

            
536
	/// Returns true if a [ScheduledRequest] exists for a given delegation
537
6
	pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
538
6
		!<DelegationScheduledRequests<T>>::get(collator, delegator).is_empty()
539
6
	}
540

            
541
	/// Returns true if a [DelegationAction::Revoke] [ScheduledRequest] exists for a given delegation
542
26
	pub fn delegation_request_revoke_exists(
543
26
		collator: &T::AccountId,
544
26
		delegator: &T::AccountId,
545
26
	) -> bool {
546
26
		<DelegationScheduledRequests<T>>::get(collator, delegator)
547
26
			.iter()
548
26
			.any(|req| matches!(req.action, DelegationAction::Revoke(_)))
549
26
	}
550
}
551

            
552
#[cfg(test)]
553
mod tests {
554
	use super::*;
555
	use crate::{mock::Test, set::OrderedSet, Bond};
556

            
557
	#[test]
558
1
	fn test_cancel_request_with_state_removes_request_for_correct_delegator_and_updates_state() {
559
1
		let mut state = Delegator {
560
1
			id: 1,
561
1
			delegations: OrderedSet::from(vec![Bond {
562
1
				amount: 100,
563
1
				owner: 2,
564
1
			}]),
565
1
			total: 100,
566
1
			less_total: 150,
567
1
			status: crate::DelegatorStatus::Active,
568
1
		};
569
1
		let mut scheduled_requests = vec![
570
1
			ScheduledRequest {
571
1
				when_executable: 1,
572
1
				action: DelegationAction::Revoke(100),
573
1
			},
574
1
			ScheduledRequest {
575
1
				when_executable: 1,
576
1
				action: DelegationAction::Decrease(50),
577
1
			},
578
		]
579
1
		.try_into()
580
1
		.expect("must succeed");
581
1
		let removed_request =
582
1
			<Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
583

            
584
1
		assert_eq!(
585
			removed_request,
586
			Some(ScheduledRequest {
587
				when_executable: 1,
588
				action: DelegationAction::Revoke(100),
589
			})
590
		);
591
1
		assert_eq!(
592
			scheduled_requests,
593
1
			vec![ScheduledRequest {
594
1
				when_executable: 1,
595
1
				action: DelegationAction::Decrease(50),
596
1
			},]
597
		);
598
1
		assert_eq!(
599
			state.less_total, 50,
600
			"less_total should be reduced by the amount of the cancelled request"
601
		);
602
1
	}
603

            
604
	#[test]
605
1
	fn test_cancel_request_with_state_does_nothing_when_request_does_not_exist() {
606
1
		let mut state = Delegator {
607
1
			id: 1,
608
1
			delegations: OrderedSet::from(vec![Bond {
609
1
				amount: 100,
610
1
				owner: 2,
611
1
			}]),
612
1
			total: 100,
613
1
			less_total: 100,
614
1
			status: crate::DelegatorStatus::Active,
615
1
		};
616
1
		let mut scheduled_requests: BoundedVec<
617
1
			ScheduledRequest<u128>,
618
1
			<Test as crate::pallet::Config>::MaxScheduledRequestsPerDelegator,
619
1
		> = BoundedVec::default();
620
1
		let removed_request =
621
1
			<Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
622

            
623
1
		assert_eq!(removed_request, None,);
624
1
		assert_eq!(
625
1
			scheduled_requests.len(),
626
			0,
627
			"scheduled_requests should remain empty"
628
		);
629
1
		assert_eq!(
630
			state.less_total, 100,
631
			"less_total should remain unchanged when there is nothing to cancel"
632
		);
633
1
	}
634
}