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};
31
use scale_info::TypeInfo;
32
use sp_runtime::{traits::Saturating, RuntimeDebug};
33

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

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

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

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

            
89
impl<A, B> From<ScheduledRequest<A, B>> for CancelledScheduledRequest<B> {
90
10
	fn from(request: ScheduledRequest<A, B>) -> Self {
91
10
		CancelledScheduledRequest {
92
10
			when_executable: request.when_executable,
93
10
			action: request.action,
94
10
		}
95
10
	}
96
}
97

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

            
107
50
		let actual_weight =
108
50
			<T as Config>::WeightInfo::schedule_revoke_delegation(scheduled_requests.len() as u32);
109
50

            
110
50
		// If this is the first scheduled request for this delegator towards this collator,
111
50
		// ensure we do not exceed the maximum number of delegators that can have pending
112
50
		// requests for the collator.
113
50
		let is_new_delegator =
114
50
			!<DelegationScheduledRequests<T>>::contains_key(&collator, &delegator);
115
50
		if is_new_delegator {
116
49
			let current = <DelegationScheduledRequestsPerCollator<T>>::get(&collator);
117
49
			if current >= Pallet::<T>::max_delegators_per_candidate() {
118
				return Err(DispatchErrorWithPostInfo {
119
					post_info: Some(actual_weight).into(),
120
					error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
121
				});
122
49
			}
123
1
		}
124

            
125
50
		ensure!(
126
50
			scheduled_requests.is_empty(),
127
1
			DispatchErrorWithPostInfo {
128
1
				post_info: Some(actual_weight).into(),
129
1
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
130
1
			},
131
		);
132

            
133
49
		let bonded_amount = state
134
49
			.get_bond_amount(&collator)
135
49
			.ok_or(<Error<T>>::DelegationDNE)?;
136
48
		let now = <Round<T>>::get().current;
137
48
		let when = now.saturating_add(T::RevokeDelegationDelay::get());
138
48
		scheduled_requests
139
48
			.try_push(ScheduledRequest {
140
48
				delegator: delegator.clone(),
141
48
				action: DelegationAction::Revoke(bonded_amount),
142
48
				when_executable: when,
143
48
			})
144
48
			.map_err(|_| DispatchErrorWithPostInfo {
145
				post_info: Some(actual_weight).into(),
146
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
147
48
			})?;
148
48
		state.less_total = state.less_total.saturating_add(bonded_amount);
149
48
		if is_new_delegator {
150
48
			<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
151
48
				*c = c.saturating_add(1);
152
48
			});
153
48
		}
154
48
		<DelegationScheduledRequests<T>>::insert(
155
48
			collator.clone(),
156
48
			delegator.clone(),
157
48
			scheduled_requests,
158
48
		);
159
48
		<DelegatorState<T>>::insert(delegator.clone(), state);
160
48

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

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

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

            
183
88
		// If this is the first scheduled request for this delegator towards this collator,
184
88
		// ensure we do not exceed the maximum number of delegators that can have pending
185
88
		// requests for the collator.
186
88
		let is_new_delegator =
187
88
			!<DelegationScheduledRequests<T>>::contains_key(&collator, &delegator);
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
81
		// 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
				delegator: delegator.clone(),
248
81
				action: DelegationAction::Decrease(decrease_amount),
249
81
				when_executable: when,
250
81
			})
251
81
			.map_err(|_| DispatchErrorWithPostInfo {
252
1
				post_info: Some(actual_weight).into(),
253
1
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
254
81
			})?;
255
80
		state.less_total = state.less_total.saturating_add(decrease_amount);
256
80
		if is_new_delegator {
257
28
			<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
258
28
				*c = c.saturating_add(1);
259
28
			});
260
55
		}
261
80
		<DelegationScheduledRequests<T>>::insert(
262
80
			collator.clone(),
263
80
			delegator.clone(),
264
80
			scheduled_requests,
265
80
		);
266
80
		<DelegatorState<T>>::insert(delegator.clone(), state);
267
80

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

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

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

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

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

            
316
12
	fn cancel_request_with_state(
317
12
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
318
12
		scheduled_requests: &mut BoundedVec<
319
12
			ScheduledRequest<T::AccountId, BalanceOf<T>>,
320
12
			T::MaxScheduledRequestsPerDelegator,
321
12
		>,
322
12
	) -> Option<ScheduledRequest<T::AccountId, BalanceOf<T>>> {
323
12
		let request = scheduled_requests.get(0).cloned()?;
324
11
		scheduled_requests.remove(0);
325
11
		let amount = request.action.amount();
326
11
		state.less_total = state.less_total.saturating_sub(amount);
327
11
		Some(request)
328
12
	}
329

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

            
341
39
		let now = <Round<T>>::get().current;
342
39
		ensure!(
343
39
			request.when_executable <= now,
344
			<Error<T>>::PendingDelegationRequestNotDueYet
345
		);
346

            
347
39
		match request.action {
348
23
			DelegationAction::Revoke(amount) => {
349
23
				let actual_weight =
350
23
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
351

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

            
366
				// remove from pending requests
367
23
				let amount = scheduled_requests.remove(0).action.amount();
368
23
				state.less_total = state.less_total.saturating_sub(amount);
369
23

            
370
23
				// remove delegation from delegator state
371
23
				state.rm_delegation::<T>(&collator);
372
23

            
373
23
				// remove delegation from auto-compounding info
374
23
				<AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
375
23

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

            
414
16
				// remove from pending requests
415
16
				let amount = scheduled_requests.remove(0).action.amount();
416
16
				state.less_total = state.less_total.saturating_sub(amount);
417

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

            
431
16
							state
432
16
								.total_sub_if::<T, _>(amount, |total| {
433
16
									let new_total: BalanceOf<T> = total.into();
434
16
									ensure!(
435
16
										new_total >= T::MinDelegation::get(),
436
										<Error<T>>::DelegationBelowMin
437
									);
438

            
439
16
									Ok(())
440
16
								})
441
16
								.map_err(|err| DispatchErrorWithPostInfo {
442
									post_info: Some(actual_weight).into(),
443
									error: err,
444
16
								})?;
445

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

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

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

            
511
19
		if scheduled_requests.is_empty() {
512
15
			return;
513
4
		}
514
4

            
515
4
		let mut total_amount: BalanceOf<T> = Default::default();
516
4
		for request in scheduled_requests.iter() {
517
4
			let amount = request.action.amount();
518
4
			total_amount = total_amount.saturating_add(amount);
519
4
		}
520

            
521
4
		state.less_total = state.less_total.saturating_sub(total_amount);
522
4
		<DelegationScheduledRequests<T>>::remove(collator, delegator);
523
4
		<DelegationScheduledRequestsPerCollator<T>>::mutate(collator, |c| {
524
4
			*c = c.saturating_sub(1);
525
4
		});
526
19
	}
527

            
528
	/// Returns true if a [ScheduledRequest] exists for a given delegation
529
6
	pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
530
6
		!<DelegationScheduledRequests<T>>::get(collator, delegator).is_empty()
531
6
	}
532

            
533
	/// Returns true if a [DelegationAction::Revoke] [ScheduledRequest] exists for a given delegation
534
26
	pub fn delegation_request_revoke_exists(
535
26
		collator: &T::AccountId,
536
26
		delegator: &T::AccountId,
537
26
	) -> bool {
538
26
		<DelegationScheduledRequests<T>>::get(collator, delegator)
539
26
			.iter()
540
26
			.any(|req| matches!(req.action, DelegationAction::Revoke(_)))
541
26
	}
542
}
543

            
544
#[cfg(test)]
545
mod tests {
546
	use super::*;
547
	use crate::{mock::Test, set::OrderedSet, Bond};
548

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

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

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

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