1
// Copyright 2025 Moonbeam Foundation.
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
//! Test utilities for crowdloan-rewards pallet
18

            
19
use crate::{self as pallet_crowdloan_rewards, Config};
20
use frame_support::{
21
	construct_runtime, parameter_types,
22
	traits::{Everything, OnFinalize, OnInitialize},
23
	weights::{constants::RocksDbWeight, Weight},
24
	PalletId,
25
};
26
use sp_core::{crypto::AccountId32, ed25519, Pair, H256};
27
use sp_runtime::{
28
	traits::{BlakeTwo256, BlockNumberProvider, IdentityLookup},
29
	BuildStorage, Perbill,
30
};
31

            
32
pub type AccountId = AccountId32;
33
pub type Balance = u128;
34

            
35
// Configure a mock runtime to test the pallet.
36
construct_runtime!(
37
	pub enum Test
38
	{
39
		System: frame_system,
40
		Balances: pallet_balances,
41
		CrowdloanRewards: pallet_crowdloan_rewards,
42
		Utility: pallet_utility,
43
	}
44
);
45

            
46
parameter_types! {
47
	pub const BlockHashCount: u32 = 250;
48
	pub const MaximumBlockWeight: Weight = Weight::from_parts(1024, 1);
49
	pub const MaximumBlockLength: u32 = 2 * 1024;
50
	pub const SS58Prefix: u8 = 42;
51
}
52

            
53
impl frame_system::Config for Test {
54
	type BaseCallFilter = Everything;
55
	type DbWeight = RocksDbWeight;
56
	type RuntimeOrigin = RuntimeOrigin;
57
	type RuntimeTask = RuntimeTask;
58
	type Nonce = u64;
59
	type Block = frame_system::mocking::MockBlockU32<Test>;
60
	type RuntimeCall = RuntimeCall;
61
	type Hash = H256;
62
	type Hashing = BlakeTwo256;
63
	type AccountId = AccountId;
64
	type Lookup = IdentityLookup<Self::AccountId>;
65
	type RuntimeEvent = RuntimeEvent;
66
	type BlockHashCount = BlockHashCount;
67
	type Version = ();
68
	type PalletInfo = PalletInfo;
69
	type AccountData = pallet_balances::AccountData<Balance>;
70
	type OnNewAccount = ();
71
	type OnKilledAccount = ();
72
	type SystemWeightInfo = ();
73
	type BlockWeights = ();
74
	type BlockLength = ();
75
	type SS58Prefix = SS58Prefix;
76
	type OnSetCode = ();
77
	type MaxConsumers = frame_support::traits::ConstU32<16>;
78
	type SingleBlockMigrations = ();
79
	type MultiBlockMigrator = ();
80
	type PreInherents = ();
81
	type PostInherents = ();
82
	type PostTransactions = ();
83
	type ExtensionsWeightInfo = ();
84
}
85

            
86
parameter_types! {
87
	pub const ExistentialDeposit: u128 = 1;
88
}
89

            
90
impl pallet_balances::Config for Test {
91
	type MaxReserves = ();
92
	type ReserveIdentifier = [u8; 4];
93
	type MaxLocks = ();
94
	type Balance = Balance;
95
	type RuntimeEvent = RuntimeEvent;
96
	type DustRemoval = ();
97
	type ExistentialDeposit = ExistentialDeposit;
98
	type AccountStore = System;
99
	type WeightInfo = ();
100
	type RuntimeHoldReason = ();
101
	type FreezeIdentifier = ();
102
	type MaxFreezes = ();
103
	type RuntimeFreezeReason = ();
104
	type DoneSlashHandler = ();
105
}
106

            
107
pub struct MockVestingBlockNumberProvider;
108
impl BlockNumberProvider for MockVestingBlockNumberProvider {
109
	type BlockNumber = u32;
110

            
111
32
	fn current_block_number() -> Self::BlockNumber {
112
32
		System::block_number()
113
32
	}
114

            
115
	#[cfg(feature = "runtime-benchmarks")]
116
	fn set_block_number(n: Self::BlockNumber) {
117
		frame_system::Pallet::<Test>::set_block_number(n);
118
	}
119
}
120

            
121
parameter_types! {
122
	pub const Initialized: bool = false;
123
	pub const InitializationPayment: Perbill = Perbill::from_percent(20);
124
	pub const MaxInitContributors: u32 = 8;
125
	pub const MinimumReward: Balance = 0;
126
	pub const RewardAddressRelayVoteThreshold: Perbill = Perbill::from_percent(60);
127
	pub const SignatureNetworkIdentifier: &'static [u8] = b"TEST_NET";
128
	pub const CrowdloanPalletId: PalletId = PalletId(*b"Crowdloa");
129
}
130

            
131
impl Config for Test {
132
	type Initialized = Initialized;
133
	type InitializationPayment = InitializationPayment;
134
	type MaxInitContributors = MaxInitContributors;
135
	type MinimumReward = MinimumReward;
136
	type RewardAddressRelayVoteThreshold = RewardAddressRelayVoteThreshold;
137
	type RewardCurrency = Balances;
138
	type RelayChainAccountId = AccountId;
139
	type RewardAddressChangeOrigin = frame_system::EnsureSigned<AccountId>;
140
	type SignatureNetworkIdentifier = SignatureNetworkIdentifier;
141
	type RewardAddressAssociateOrigin = frame_system::EnsureSigned<AccountId>;
142
	type VestingBlockNumber = u32;
143
	type VestingBlockProvider = MockVestingBlockNumberProvider;
144
	type WeightInfo = ();
145
}
146

            
147
impl pallet_utility::Config for Test {
148
	type RuntimeEvent = RuntimeEvent;
149
	type RuntimeCall = RuntimeCall;
150
	type WeightInfo = ();
151
	type PalletsOrigin = OriginCaller;
152
}
153

            
154
pub(crate) struct ExtBuilder {
155
	funded_accounts: Vec<(AccountId, Option<AccountId>, Balance)>,
156
	init_vesting_block: u32,
157
	end_vesting_block: u32,
158
}
159

            
160
impl Default for ExtBuilder {
161
10
	fn default() -> Self {
162
10
		Self {
163
10
			funded_accounts: vec![
164
10
				// Associated account with rewards
165
10
				(
166
10
					AccountId::from([10u8; 32]),      // relay account
167
10
					Some(AccountId::from([1u8; 32])), // native account
168
10
					10_000u128,                       // reward
169
10
				),
170
10
			],
171
10
			init_vesting_block: 1u32,
172
10
			end_vesting_block: 100u32,
173
10
		}
174
10
	}
175
}
176

            
177
impl ExtBuilder {
178
19
	pub(crate) fn empty() -> sp_io::TestExternalities {
179
		// Default for empty genesis (enough for typical manual initialization tests)
180
19
		Self {
181
19
			funded_accounts: vec![],
182
19
			init_vesting_block: 1u32,
183
19
			end_vesting_block: 100u32,
184
19
		}
185
19
		.inner_build(2501)
186
19
	}
187

            
188
5
	pub(crate) fn with_funded_accounts(
189
5
		mut self,
190
5
		funded_accounts: Vec<(AccountId, Option<AccountId>, Balance)>,
191
5
	) -> Self {
192
5
		self.funded_accounts = funded_accounts;
193
5
		self
194
5
	}
195

            
196
10
	pub(crate) fn build(self) -> sp_io::TestExternalities {
197
		// Calculate pot size based on genesis rewards
198
10
		let total_rewards: Balance = self
199
10
			.funded_accounts
200
10
			.iter()
201
10
			.map(|(_, _, amount)| *amount)
202
10
			.sum();
203

            
204
		// Add small dust for funded genesis (dust must be < contributors for validation)
205
10
		self.inner_build(total_rewards + 1)
206
10
	}
207

            
208
29
	fn inner_build(self, pot_size: u128) -> sp_io::TestExternalities {
209
29
		let mut t = frame_system::GenesisConfig::<Test>::default()
210
29
			.build_storage()
211
29
			.unwrap();
212

            
213
29
		pallet_balances::GenesisConfig::<Test> {
214
29
			balances: vec![
215
29
				// Pallet account with initial funds
216
29
				(CrowdloanRewards::account_id(), pot_size),
217
29
				// Test accounts
218
29
				(AccountId::from([1u8; 32]), 100_000),
219
29
				(AccountId::from([2u8; 32]), 100_000),
220
29
				(AccountId::from([3u8; 32]), 100_000),
221
29
			],
222
29
			dev_accounts: None,
223
29
		}
224
29
		.assimilate_storage(&mut t)
225
29
		.unwrap();
226

            
227
29
		crate::GenesisConfig::<Test> {
228
29
			funded_accounts: self.funded_accounts,
229
29
			init_vesting_block: self.init_vesting_block,
230
29
			end_vesting_block: self.end_vesting_block,
231
29
		}
232
29
		.assimilate_storage(&mut t)
233
29
		.unwrap();
234

            
235
29
		t.into()
236
29
	}
237
}
238

            
239
// Helper functions for test setup
240
6
pub fn run_to_block(n: u32) {
241
266
	while System::block_number() < n {
242
260
		CrowdloanRewards::on_finalize(System::block_number());
243
260
		Balances::on_finalize(System::block_number());
244
260
		System::on_finalize(System::block_number());
245
260
		System::set_block_number(System::block_number() + 1);
246
260
		System::on_initialize(System::block_number());
247
260
		Balances::on_initialize(System::block_number());
248
260
		CrowdloanRewards::on_initialize(System::block_number());
249
260
	}
250
6
}
251

            
252
15
pub(crate) fn get_ed25519_pairs(num: u32) -> Vec<ed25519::Pair> {
253
15
	let seed: u128 = 12345678901234567890123456789012;
254
15
	let mut pairs = Vec::new();
255
45
	for i in 0..num {
256
45
		pairs.push(ed25519::Pair::from_seed(
257
45
			(seed.clone() + i as u128)
258
45
				.to_string()
259
45
				.as_bytes()
260
45
				.try_into()
261
45
				.unwrap(),
262
		))
263
	}
264
15
	pairs
265
15
}
266

            
267
// Helper function to create a test account
268
228
pub(crate) fn account(id: u8) -> AccountId32 {
269
228
	AccountId32::from([id; 32])
270
228
}
271

            
272
44
pub(crate) fn roll_to(n: u32) {
273
44
	let mut current_block_number = System::block_number();
274
486
	while current_block_number < n {
275
442
		CrowdloanRewards::on_initialize(System::block_number());
276
442
		System::set_block_number(current_block_number);
277
442
		CrowdloanRewards::on_finalize(System::block_number());
278
442
		current_block_number = current_block_number.saturating_add(1);
279
442
	}
280
44
}
281

            
282
8
pub(crate) fn events() -> Vec<super::Event<Test>> {
283
8
	System::events()
284
8
		.into_iter()
285
8
		.map(|r| r.event)
286
94
		.filter_map(|e| {
287
94
			if let RuntimeEvent::CrowdloanRewards(inner) = e {
288
42
				Some(inner)
289
			} else {
290
52
				None
291
			}
292
94
		})
293
8
		.collect::<Vec<_>>()
294
8
}