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, DelegationScheduledRequestsSummaryMap, DelegatorState,
22
	Error, Event, Pallet, Round, 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
54
	pub fn amount(&self) -> Balance {
59
54
		match self {
60
33
			DelegationAction::Revoke(amount) => *amount,
61
21
			DelegationAction::Decrease(amount) => *amount,
62
		}
63
54
	}
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
		<DelegationScheduledRequestsSummaryMap<T>>::insert(
161
48
			collator.clone(),
162
48
			delegator.clone(),
163
48
			DelegationAction::Revoke(bonded_amount),
164
		);
165
48
		<DelegatorState<T>>::insert(delegator.clone(), state);
166

            
167
48
		Self::deposit_event(Event::DelegationRevocationScheduled {
168
48
			round: now,
169
48
			delegator,
170
48
			candidate: collator,
171
48
			scheduled_exit: when,
172
48
		});
173
48
		Ok(().into())
174
52
	}
175

            
176
	/// Schedules a [DelegationAction::Decrease] for the delegator, towards a given collator.
177
91
	pub(crate) fn delegation_schedule_bond_decrease(
178
91
		collator: T::AccountId,
179
91
		delegator: T::AccountId,
180
91
		decrease_amount: BalanceOf<T>,
181
91
	) -> DispatchResultWithPostInfo {
182
91
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
183
90
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
184

            
185
90
		let actual_weight = <T as Config>::WeightInfo::schedule_delegator_bond_less(
186
90
			scheduled_requests.len() as u32,
187
		);
188

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

            
204
89
		ensure!(
205
89
			!scheduled_requests
206
89
				.iter()
207
1285
				.any(|req| matches!(req.action, DelegationAction::Revoke(_))),
208
2
			DispatchErrorWithPostInfo {
209
2
				post_info: Some(actual_weight).into(),
210
2
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
211
2
			},
212
		);
213

            
214
87
		let bonded_amount = state
215
87
			.get_bond_amount(&collator)
216
87
			.ok_or(DispatchErrorWithPostInfo {
217
87
				post_info: Some(actual_weight).into(),
218
87
				error: <Error<T>>::DelegationDNE.into(),
219
87
			})?;
220
		// Per-request safety: a single decrease cannot exceed the current delegation
221
		// and must leave at least MinDelegation on that delegation.
222
85
		ensure!(
223
85
			bonded_amount > decrease_amount,
224
1
			DispatchErrorWithPostInfo {
225
1
				post_info: Some(actual_weight).into(),
226
1
				error: <Error<T>>::DelegatorBondBelowMin.into(),
227
1
			},
228
		);
229

            
230
		// Cumulative safety: multiple pending Decrease requests for the same
231
		// (collator, delegator) pair must also respect the MinDelegation
232
		// constraint when applied together. Otherwise, snapshots can become
233
		// inconsistent even if each request, in isolation, appears valid.
234
84
		let pending_decrease_total: BalanceOf<T> = scheduled_requests
235
84
			.iter()
236
1283
			.filter_map(|req| match req.action {
237
1280
				DelegationAction::Decrease(amount) => Some(amount),
238
				_ => None,
239
1280
			})
240
1283
			.fold(BalanceOf::<T>::zero(), |acc, amount| {
241
1280
				acc.saturating_add(amount)
242
1280
			});
243
84
		let total_decrease_after = pending_decrease_total.saturating_add(decrease_amount);
244
84
		let new_amount_after_all = bonded_amount.saturating_sub(total_decrease_after);
245
84
		ensure!(
246
84
			new_amount_after_all >= T::MinDelegation::get(),
247
2
			DispatchErrorWithPostInfo {
248
2
				post_info: Some(actual_weight).into(),
249
2
				error: <Error<T>>::DelegationBelowMin.into(),
250
2
			},
251
		);
252

            
253
		// Net Total is total after pending orders are executed
254
82
		let net_total = state.total().saturating_sub(state.less_total);
255
		// Net Total is always >= MinDelegation
256
82
		let max_subtracted_amount = net_total.saturating_sub(T::MinDelegation::get().into());
257
82
		ensure!(
258
82
			decrease_amount <= max_subtracted_amount,
259
			DispatchErrorWithPostInfo {
260
				post_info: Some(actual_weight).into(),
261
				error: <Error<T>>::DelegatorBondBelowMin.into(),
262
			},
263
		);
264

            
265
82
		let now = <Round<T>>::get().current;
266
82
		let when = now.saturating_add(T::DelegationBondLessDelay::get());
267
82
		scheduled_requests
268
82
			.try_push(ScheduledRequest {
269
82
				action: DelegationAction::Decrease(decrease_amount),
270
82
				when_executable: when,
271
82
			})
272
82
			.map_err(|_| DispatchErrorWithPostInfo {
273
1
				post_info: Some(actual_weight).into(),
274
1
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
275
1
			})?;
276
81
		state.less_total = state.less_total.saturating_add(decrease_amount);
277
81
		if is_new_delegator {
278
29
			<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
279
29
				*c = c.saturating_add(1);
280
29
			});
281
52
		}
282
81
		<DelegationScheduledRequests<T>>::insert(
283
81
			collator.clone(),
284
81
			delegator.clone(),
285
81
			scheduled_requests,
286
		);
287
		// Update summary map: accumulate the new decrease amount
288
81
		<DelegationScheduledRequestsSummaryMap<T>>::mutate(&collator, &delegator, |entry| {
289
81
			*entry = Some(match entry.take() {
290
52
				Some(DelegationAction::Decrease(existing)) => {
291
52
					DelegationAction::Decrease(existing.saturating_add(decrease_amount))
292
				}
293
29
				_ => DelegationAction::Decrease(decrease_amount),
294
			});
295
81
		});
296
81
		<DelegatorState<T>>::insert(delegator.clone(), state);
297

            
298
81
		Self::deposit_event(Event::DelegationDecreaseScheduled {
299
81
			delegator,
300
81
			candidate: collator,
301
81
			amount_to_decrease: decrease_amount,
302
81
			execute_round: when,
303
81
		});
304
81
		Ok(Some(actual_weight).into())
305
91
	}
306

            
307
	/// Cancels the delegator's existing [ScheduledRequest] towards a given collator.
308
10
	pub(crate) fn delegation_cancel_request(
309
10
		collator: T::AccountId,
310
10
		delegator: T::AccountId,
311
10
	) -> DispatchResultWithPostInfo {
312
10
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
313
10
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
314
10
		let actual_weight =
315
10
			<T as Config>::WeightInfo::cancel_delegation_request(scheduled_requests.len() as u32);
316

            
317
10
		let request = Self::cancel_request_with_state(&mut state, &mut scheduled_requests).ok_or(
318
10
			DispatchErrorWithPostInfo {
319
10
				post_info: Some(actual_weight).into(),
320
10
				error: <Error<T>>::PendingDelegationRequestDNE.into(),
321
10
			},
322
		)?;
323

            
324
10
		match &request.action {
325
6
			DelegationAction::Revoke(_) => {
326
6
				<DelegationScheduledRequestsSummaryMap<T>>::remove(&collator, &delegator);
327
6
			}
328
4
			DelegationAction::Decrease(amount) => {
329
4
				let amount = *amount;
330
4
				<DelegationScheduledRequestsSummaryMap<T>>::mutate_exists(
331
4
					&collator,
332
4
					&delegator,
333
4
					|entry| {
334
4
						if let Some(DelegationAction::Decrease(existing)) = entry {
335
4
							let remaining = existing.saturating_sub(amount);
336
4
							if remaining.is_zero() {
337
4
								*entry = None;
338
4
							} else {
339
								*existing = remaining;
340
							}
341
						}
342
4
					},
343
				);
344
			}
345
		}
346

            
347
10
		if scheduled_requests.is_empty() {
348
10
			<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
349
10
				*c = c.saturating_sub(1);
350
10
			});
351
10
			<DelegationScheduledRequests<T>>::remove(&collator, &delegator);
352
		} else {
353
			<DelegationScheduledRequests<T>>::insert(
354
				collator.clone(),
355
				delegator.clone(),
356
				scheduled_requests,
357
			);
358
		}
359
10
		<DelegatorState<T>>::insert(delegator.clone(), state);
360

            
361
10
		Self::deposit_event(Event::CancelledDelegationRequest {
362
10
			delegator,
363
10
			collator,
364
10
			cancelled_request: request.into(),
365
10
		});
366
10
		Ok(Some(actual_weight).into())
367
10
	}
368

            
369
12
	fn cancel_request_with_state(
370
12
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
371
12
		scheduled_requests: &mut BoundedVec<
372
12
			ScheduledRequest<BalanceOf<T>>,
373
12
			T::MaxScheduledRequestsPerDelegator,
374
12
		>,
375
12
	) -> Option<ScheduledRequest<BalanceOf<T>>> {
376
12
		if scheduled_requests.is_empty() {
377
1
			return None;
378
11
		}
379

            
380
		// `BoundedVec::remove` can panic, but we make sure it will not happen by
381
		// checking above that `scheduled_requests` is not empty.
382
11
		let request = scheduled_requests.remove(0);
383
11
		let amount = request.action.amount();
384
11
		state.less_total = state.less_total.saturating_sub(amount);
385
11
		Some(request)
386
12
	}
387

            
388
	/// Executes the delegator's existing [ScheduledRequest] towards a given collator.
389
39
	pub(crate) fn delegation_execute_scheduled_request(
390
39
		collator: T::AccountId,
391
39
		delegator: T::AccountId,
392
39
	) -> DispatchResultWithPostInfo {
393
39
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
394
39
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
395
39
		let request = scheduled_requests
396
39
			.first()
397
39
			.ok_or(<Error<T>>::PendingDelegationRequestDNE)?;
398

            
399
39
		let now = <Round<T>>::get().current;
400
39
		ensure!(
401
39
			request.when_executable <= now,
402
			<Error<T>>::PendingDelegationRequestNotDueYet
403
		);
404

            
405
39
		match request.action {
406
23
			DelegationAction::Revoke(amount) => {
407
23
				let actual_weight =
408
23
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
409

            
410
				// revoking last delegation => leaving set of delegators
411
23
				let leaving = if state.delegations.0.len() == 1usize {
412
12
					true
413
				} else {
414
11
					ensure!(
415
11
						state.total().saturating_sub(T::MinDelegation::get().into()) >= amount,
416
						DispatchErrorWithPostInfo {
417
							post_info: Some(actual_weight).into(),
418
							error: <Error<T>>::DelegatorBondBelowMin.into(),
419
						}
420
					);
421
11
					false
422
				};
423

            
424
				// remove from pending requests
425
				// `BoundedVec::remove` can panic, but we make sure it will not happen by checking above that `scheduled_requests` is not empty.
426
23
				let amount = scheduled_requests.remove(0).action.amount();
427
23
				state.less_total = state.less_total.saturating_sub(amount);
428

            
429
				// remove delegation from delegator state
430
23
				state.rm_delegation::<T>(&collator);
431

            
432
				// remove delegation from auto-compounding info
433
23
				<AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
434

            
435
				// clear the summary entry
436
23
				<DelegationScheduledRequestsSummaryMap<T>>::remove(&collator, &delegator);
437

            
438
				// remove delegation from collator state delegations
439
23
				Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), amount)
440
23
					.map_err(|err| DispatchErrorWithPostInfo {
441
						post_info: Some(actual_weight).into(),
442
						error: err,
443
					})?;
444
23
				Self::deposit_event(Event::DelegationRevoked {
445
23
					delegator: delegator.clone(),
446
23
					candidate: collator.clone(),
447
23
					unstaked_amount: amount,
448
23
				});
449
23
				if scheduled_requests.is_empty() {
450
23
					<DelegationScheduledRequests<T>>::remove(&collator, &delegator);
451
23
					<DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
452
23
						*c = c.saturating_sub(1);
453
23
					});
454
				} else {
455
					<DelegationScheduledRequests<T>>::insert(
456
						collator.clone(),
457
						delegator.clone(),
458
						scheduled_requests,
459
					);
460
				}
461
23
				if leaving {
462
12
					<DelegatorState<T>>::remove(&delegator);
463
12
					Self::deposit_event(Event::DelegatorLeft {
464
12
						delegator,
465
12
						unstaked_amount: amount,
466
12
					});
467
12
				} else {
468
11
					<DelegatorState<T>>::insert(&delegator, state);
469
11
				}
470
23
				Ok(Some(actual_weight).into())
471
			}
472
			DelegationAction::Decrease(_) => {
473
16
				let actual_weight =
474
16
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
475

            
476
				// remove from pending requests
477
				// `BoundedVec::remove` can panic, but we make sure it will not happen by checking above that `scheduled_requests` is not empty.
478
16
				let amount = scheduled_requests.remove(0).action.amount();
479
16
				state.less_total = state.less_total.saturating_sub(amount);
480

            
481
				// decrease delegation
482
16
				for bond in &mut state.delegations.0 {
483
16
					if bond.owner == collator {
484
16
						return if bond.amount > amount {
485
16
							let amount_before: BalanceOf<T> = bond.amount.into();
486
16
							bond.amount = bond.amount.saturating_sub(amount);
487
16
							let mut collator_info = <CandidateInfo<T>>::get(&collator)
488
16
								.ok_or(<Error<T>>::CandidateDNE)
489
16
								.map_err(|err| DispatchErrorWithPostInfo {
490
									post_info: Some(actual_weight).into(),
491
									error: err.into(),
492
								})?;
493

            
494
16
							state
495
16
								.total_sub_if::<T, _>(amount, |total| {
496
16
									let new_total: BalanceOf<T> = total.into();
497
16
									ensure!(
498
16
										new_total >= T::MinDelegation::get(),
499
										<Error<T>>::DelegationBelowMin
500
									);
501

            
502
16
									Ok(())
503
16
								})
504
16
								.map_err(|err| DispatchErrorWithPostInfo {
505
									post_info: Some(actual_weight).into(),
506
									error: err,
507
								})?;
508

            
509
							// need to go into decrease_delegation
510
16
							let in_top = collator_info
511
16
								.decrease_delegation::<T>(
512
16
									&collator,
513
16
									delegator.clone(),
514
16
									amount_before,
515
16
									amount,
516
								)
517
16
								.map_err(|err| DispatchErrorWithPostInfo {
518
									post_info: Some(actual_weight).into(),
519
									error: err,
520
								})?;
521
16
							<CandidateInfo<T>>::insert(&collator, collator_info);
522
16
							let new_total_staked = <Total<T>>::get().saturating_sub(amount);
523
16
							<Total<T>>::put(new_total_staked);
524

            
525
16
							if scheduled_requests.is_empty() {
526
15
								<DelegationScheduledRequests<T>>::remove(&collator, &delegator);
527
15
								<DelegationScheduledRequestsPerCollator<T>>::mutate(
528
15
									&collator,
529
15
									|c| {
530
15
										*c = c.saturating_sub(1);
531
15
									},
532
								);
533
1
							} else {
534
1
								<DelegationScheduledRequests<T>>::insert(
535
1
									collator.clone(),
536
1
									delegator.clone(),
537
1
									scheduled_requests,
538
1
								);
539
1
							}
540
							// Update summary map: subtract executed decrease
541
16
							<DelegationScheduledRequestsSummaryMap<T>>::mutate_exists(
542
16
								&collator,
543
16
								&delegator,
544
16
								|entry| {
545
16
									if let Some(DelegationAction::Decrease(existing)) = entry {
546
16
										let remaining = existing.saturating_sub(amount);
547
16
										if remaining.is_zero() {
548
15
											*entry = None;
549
15
										} else {
550
1
											*existing = remaining;
551
1
										}
552
									}
553
16
								},
554
							);
555
16
							<DelegatorState<T>>::insert(delegator.clone(), state);
556
16
							Self::deposit_event(Event::DelegationDecreased {
557
16
								delegator,
558
16
								candidate: collator.clone(),
559
16
								amount,
560
16
								in_top,
561
16
							});
562
16
							Ok(Some(actual_weight).into())
563
						} else {
564
							// must rm entire delegation if bond.amount <= less or cancel request
565
							Err(DispatchErrorWithPostInfo {
566
								post_info: Some(actual_weight).into(),
567
								error: <Error<T>>::DelegationBelowMin.into(),
568
							})
569
						};
570
					}
571
				}
572
				Err(DispatchErrorWithPostInfo {
573
					post_info: Some(actual_weight).into(),
574
					error: <Error<T>>::DelegationDNE.into(),
575
				})
576
			}
577
		}
578
39
	}
579

            
580
	/// Removes the delegator's existing [ScheduledRequest] towards a given collator, if exists.
581
	/// The state needs to be persisted by the caller of this function.
582
19
	pub(crate) fn delegation_remove_request_with_state(
583
19
		collator: &T::AccountId,
584
19
		delegator: &T::AccountId,
585
19
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
586
19
	) {
587
19
		let scheduled_requests = <DelegationScheduledRequests<T>>::get(collator, delegator);
588

            
589
19
		if scheduled_requests.is_empty() {
590
15
			return;
591
4
		}
592

            
593
		// Calculate total amount across all scheduled requests
594
4
		let total_amount: BalanceOf<T> = scheduled_requests
595
4
			.iter()
596
4
			.map(|request| request.action.amount())
597
4
			.fold(BalanceOf::<T>::zero(), |acc, amount| {
598
4
				acc.saturating_add(amount)
599
4
			});
600

            
601
4
		state.less_total = state.less_total.saturating_sub(total_amount);
602
4
		<DelegationScheduledRequests<T>>::remove(collator, delegator);
603
4
		<DelegationScheduledRequestsSummaryMap<T>>::remove(collator, delegator);
604
4
		<DelegationScheduledRequestsPerCollator<T>>::mutate(collator, |c| {
605
4
			*c = c.saturating_sub(1);
606
4
		});
607
19
	}
608

            
609
	/// Returns true if a [ScheduledRequest] exists for a given delegation
610
6
	pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
611
6
		!<DelegationScheduledRequests<T>>::get(collator, delegator).is_empty()
612
6
	}
613

            
614
	/// Returns true if a [DelegationAction::Revoke] [ScheduledRequest] exists for a given delegation
615
26
	pub fn delegation_request_revoke_exists(
616
26
		collator: &T::AccountId,
617
26
		delegator: &T::AccountId,
618
26
	) -> bool {
619
23
		matches!(
620
26
			<DelegationScheduledRequestsSummaryMap<T>>::get(collator, delegator),
621
			Some(DelegationAction::Revoke(_))
622
		)
623
26
	}
624
}
625

            
626
#[cfg(test)]
627
mod tests {
628
	use super::*;
629
	use crate::{mock::Test, set::OrderedSet, Bond};
630

            
631
	#[test]
632
1
	fn test_cancel_request_with_state_removes_request_for_correct_delegator_and_updates_state() {
633
1
		let mut state = Delegator {
634
1
			id: 1,
635
1
			delegations: OrderedSet::from(vec![Bond {
636
1
				amount: 100,
637
1
				owner: 2,
638
1
			}]),
639
1
			total: 100,
640
1
			less_total: 150,
641
1
			status: crate::DelegatorStatus::Active,
642
1
		};
643
1
		let mut scheduled_requests = vec![
644
1
			ScheduledRequest {
645
1
				when_executable: 1,
646
1
				action: DelegationAction::Revoke(100),
647
1
			},
648
1
			ScheduledRequest {
649
1
				when_executable: 1,
650
1
				action: DelegationAction::Decrease(50),
651
1
			},
652
		]
653
1
		.try_into()
654
1
		.expect("must succeed");
655
1
		let removed_request =
656
1
			<Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
657

            
658
1
		assert_eq!(
659
			removed_request,
660
			Some(ScheduledRequest {
661
				when_executable: 1,
662
				action: DelegationAction::Revoke(100),
663
			})
664
		);
665
1
		assert_eq!(
666
			scheduled_requests,
667
1
			vec![ScheduledRequest {
668
1
				when_executable: 1,
669
1
				action: DelegationAction::Decrease(50),
670
1
			},]
671
		);
672
1
		assert_eq!(
673
			state.less_total, 50,
674
			"less_total should be reduced by the amount of the cancelled request"
675
		);
676
1
	}
677

            
678
	#[test]
679
1
	fn test_cancel_request_with_state_does_nothing_when_request_does_not_exist() {
680
1
		let mut state = Delegator {
681
1
			id: 1,
682
1
			delegations: OrderedSet::from(vec![Bond {
683
1
				amount: 100,
684
1
				owner: 2,
685
1
			}]),
686
1
			total: 100,
687
1
			less_total: 100,
688
1
			status: crate::DelegatorStatus::Active,
689
1
		};
690
1
		let mut scheduled_requests: BoundedVec<
691
1
			ScheduledRequest<u128>,
692
1
			<Test as crate::pallet::Config>::MaxScheduledRequestsPerDelegator,
693
1
		> = BoundedVec::default();
694
1
		let removed_request =
695
1
			<Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
696

            
697
1
		assert_eq!(removed_request, None,);
698
1
		assert_eq!(
699
1
			scheduled_requests.len(),
700
			0,
701
			"scheduled_requests should remain empty"
702
		);
703
1
		assert_eq!(
704
			state.less_total, 100,
705
			"less_total should remain unchanged when there is nothing to cancel"
706
		);
707
1
	}
708
}