mirror of
https://git.ghostchain.io/proxmio/ghost-node.git
synced 2025-12-27 03:09:56 +00:00
inital commit, which is clearly not initial
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
74
pallets/claims/Cargo.toml
Normal file
74
pallets/claims/Cargo.toml
Normal file
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "ghost-claims"
|
||||
version = "0.2.2"
|
||||
description = "Ghost balance and rank claims based on EVM actions"
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, features = ["derive"] }
|
||||
scale-info = { workspace = true, features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
rustc-hex = { workspace = true }
|
||||
libsecp256k1 = { workspace = true, default-features = false }
|
||||
|
||||
frame-benchmarking = { workspace = true, optional = true }
|
||||
frame-support = { workspace = true }
|
||||
frame-system = { workspace = true }
|
||||
|
||||
pallet-ranked-collective = { workspace = true }
|
||||
pallet-vesting = { workspace = true }
|
||||
pallet-balances = { workspace = true }
|
||||
|
||||
sp-core = { features = ["serde"], workspace = true }
|
||||
sp-runtime = { features = ["serde"], workspace = true }
|
||||
sp-io = { workspace = true }
|
||||
sp-std = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { workspace = true, default-features = true }
|
||||
libsecp256k1 = { workspace = true, default-features = true }
|
||||
serde_json = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"frame-benchmarking?/std",
|
||||
"serde/std",
|
||||
"codec/std",
|
||||
"scale-info/std",
|
||||
"libsecp256k1/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"sp-core/std",
|
||||
"sp-runtime/std",
|
||||
"sp-io/std",
|
||||
"sp-std/std",
|
||||
"pallet-ranked-collective/std",
|
||||
"pallet-vesting/std",
|
||||
"pallet-balances/std",
|
||||
"rustc-hex/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"libsecp256k1/hmac",
|
||||
"libsecp256k1/static-context",
|
||||
"frame-benchmarking/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"pallet-ranked-collective/runtime-benchmarks",
|
||||
"pallet-vesting/runtime-benchmarks",
|
||||
"pallet-balances/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"frame-support/try-runtime",
|
||||
"frame-system/try-runtime",
|
||||
"pallet-ranked-collective/try-runtime",
|
||||
"pallet-vesting/try-runtime",
|
||||
"pallet-balances/try-runtime",
|
||||
"sp-runtime/try-runtime",
|
||||
]
|
||||
56
pallets/claims/src/benchmarking.rs
Normal file
56
pallets/claims/src/benchmarking.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use frame_benchmarking::v2::*;
|
||||
|
||||
#[instance_benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
use pallet_ranked_collective::Pallet as Club;
|
||||
use frame_support::dispatch::RawOrigin;
|
||||
|
||||
#[benchmark]
|
||||
fn claim() {
|
||||
let i = 1337u32;
|
||||
let ethereum_secret_key = libsecp256k1::SecretKey::parse(
|
||||
&keccak_256(&i.to_le_bytes())).unwrap();
|
||||
let eth_address = crate::secp_utils::eth(ðereum_secret_key);
|
||||
|
||||
let balance = CurrencyOf::<T, I>::minimum_balance();
|
||||
let pseudo_account: T::AccountId = Pallet::<T, I>::into_account_id(eth_address).unwrap();
|
||||
let _ = CurrencyOf::<T, I>::deposit_creating(&pseudo_account, balance);
|
||||
Total::<T, I>::put(balance);
|
||||
|
||||
let pseudo_rank = 5u16;
|
||||
let _ = Club::<T, I>::do_add_member_to_rank(
|
||||
pseudo_account.clone(),
|
||||
pseudo_rank,
|
||||
false,
|
||||
);
|
||||
|
||||
let user_account: T::AccountId = account("user", i, 0);
|
||||
let signature = crate::secp_utils::sig::<T, I>(ðereum_secret_key, &user_account.encode());
|
||||
|
||||
let prev_balance = CurrencyOf::<T, I>::free_balance(&user_account);
|
||||
let prev_rank = Club::<T, I>::rank_of(&user_account);
|
||||
|
||||
#[extrinsic_call]
|
||||
claim(RawOrigin::Signed(user_account.clone()), eth_address, signature);
|
||||
|
||||
assert_eq!(CurrencyOf::<T, I>::free_balance(&user_account), prev_balance + balance);
|
||||
assert_eq!(CurrencyOf::<T, I>::free_balance(&pseudo_account), balance - balance);
|
||||
|
||||
let rank = match prev_rank {
|
||||
Some(current_rank) if pseudo_rank <= current_rank => Some(current_rank),
|
||||
_ => Some(pseudo_rank),
|
||||
};
|
||||
assert_eq!(Club::<T, I>::rank_of(&user_account), rank);
|
||||
assert_eq!(Club::<T, I>::rank_of(&pseudo_account), None);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::new_test_ext(),
|
||||
crate::mock::Test,
|
||||
);
|
||||
}
|
||||
313
pallets/claims/src/lib.rs
Normal file
313
pallets/claims/src/lib.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use frame_support::{
|
||||
ensure, pallet_prelude::*,
|
||||
traits::{
|
||||
Currency, ExistenceRequirement, Get, RankedMembers,
|
||||
RankedMembersSwapHandler, VestingSchedule,
|
||||
},
|
||||
DefaultNoBound,
|
||||
};
|
||||
use frame_system::pallet_prelude::*;
|
||||
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
|
||||
pub use pallet::*;
|
||||
|
||||
use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};
|
||||
use sp_runtime::traits::{CheckedSub, CheckedDiv, BlockNumberProvider};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
extern crate alloc;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::{format, string::String};
|
||||
|
||||
pub mod weights;
|
||||
pub use crate::weights::WeightInfo;
|
||||
mod tests;
|
||||
mod mock;
|
||||
mod benchmarking;
|
||||
mod secp_utils;
|
||||
|
||||
/// An ethereum address (i.e. 20 bytes, used to represent an Ethereum account).
|
||||
///
|
||||
/// This gets serialized to the 0x-prefixed hex representation.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Encode, Decode, Default, RuntimeDebug, TypeInfo)]
|
||||
pub struct EthereumAddress(pub [u8; 20]);
|
||||
|
||||
impl Serialize for EthereumAddress {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let hex: String = rustc_hex::ToHex::to_hex(&self.0[..]);
|
||||
serializer.serialize_str(&format!("0x{}", hex))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for EthereumAddress {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let base_string = String::deserialize(deserializer)?;
|
||||
let offset = if base_string.starts_with("0x") { 2 } else { 0 };
|
||||
let s = &base_string[offset..];
|
||||
if s.len() != 40 {
|
||||
Err(serde::de::Error::custom(
|
||||
"Bad length of Ethereum address (should be 42 including `0x`)",
|
||||
))?;
|
||||
}
|
||||
let raw: Vec<u8> = rustc_hex::FromHex::from_hex(s)
|
||||
.map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?;
|
||||
let mut r = Self::default();
|
||||
r.0.copy_from_slice(&raw);
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, TypeInfo)]
|
||||
pub struct EcdsaSignature(pub [u8; 65]);
|
||||
|
||||
impl PartialEq for EcdsaSignature {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
&self.0[..] == &other.0[..]
|
||||
}
|
||||
}
|
||||
|
||||
impl sp_std::fmt::Debug for EcdsaSignature {
|
||||
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
|
||||
write!(f, "EcdsaSignature({:?})", &self.0[..])
|
||||
}
|
||||
}
|
||||
|
||||
type CurrencyOf<T, I> = <<T as Config<I>>::VestingSchedule as VestingSchedule<
|
||||
<T as frame_system::Config>::AccountId
|
||||
>>::Currency;
|
||||
|
||||
type BalanceOf<T, I> = <CurrencyOf<T, I> as Currency<
|
||||
<T as frame_system::Config>::AccountId>
|
||||
>::Balance;
|
||||
|
||||
type RankOf<T, I> = <pallet_ranked_collective::Pallet::<T, I> as RankedMembers>::Rank;
|
||||
type AccountIdOf<T, I> = <pallet_ranked_collective::Pallet::<T, I> as RankedMembers>::AccountId;
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::without_storage_info]
|
||||
pub struct Pallet<T, I = ()>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config<I: 'static = ()>: frame_system::Config + pallet_ranked_collective::Config<I> {
|
||||
type RuntimeEvent: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
type VestingSchedule: VestingSchedule<Self::AccountId, Moment = BlockNumberFor<Self>>;
|
||||
type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
|
||||
type MemberSwappedHandler: RankedMembersSwapHandler<AccountIdOf<Self, I>, RankOf<Self, I>>;
|
||||
|
||||
#[pallet::constant]
|
||||
type Prefix: Get<&'static [u8]>;
|
||||
|
||||
#[pallet::constant]
|
||||
type MaximumWithdrawAmount: Get<BalanceOf<Self, I>>;
|
||||
|
||||
#[pallet::constant]
|
||||
type VestingBlocks: Get<u32>;
|
||||
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {
|
||||
Claimed {
|
||||
receiver: T::AccountId,
|
||||
donor: T::AccountId,
|
||||
amount: BalanceOf<T, I>,
|
||||
rank: Option<RankOf<T, I>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T, I = ()> {
|
||||
InvalidEthereumSignature,
|
||||
InvalidEthereumAddress,
|
||||
NoBalanceToClaim,
|
||||
AddressDecodingFailed,
|
||||
ArithmeticError,
|
||||
PotUnderflow,
|
||||
}
|
||||
|
||||
#[pallet::storage]
|
||||
pub type Total<T: Config<I>, I: 'static = ()> = StorageValue<_, BalanceOf<T, I>, ValueQuery>;
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(DefaultNoBound)]
|
||||
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
|
||||
pub total: BalanceOf<T, I>,
|
||||
pub members_and_ranks: Vec<(T::AccountId, u16)>,
|
||||
}
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
|
||||
fn build(&self) {
|
||||
let cult_accounts: Vec<_> = self
|
||||
.members_and_ranks
|
||||
.iter()
|
||||
.map(|(account_id, rank)| {
|
||||
assert!(
|
||||
pallet_ranked_collective::Pallet::<T, I>::do_add_member_to_rank(
|
||||
account_id.clone(), *rank, false).is_ok(),
|
||||
"error during adding and promotion"
|
||||
);
|
||||
account_id
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
self.members_and_ranks.len() == cult_accounts.len(),
|
||||
"duplicates in `members_and_ranks`"
|
||||
);
|
||||
|
||||
Total::<T, I>::put(self.total);
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(<T as Config<I>>::WeightInfo::claim())]
|
||||
pub fn claim(
|
||||
origin: OriginFor<T>,
|
||||
ethereum_address: EthereumAddress,
|
||||
ethereum_signature: EcdsaSignature,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let data = who.using_encoded(to_ascii_hex);
|
||||
let recovered_address = Self::recover_ethereum_address(
|
||||
ðereum_signature,
|
||||
&data,
|
||||
).ok_or(Error::<T, I>::InvalidEthereumSignature)?;
|
||||
|
||||
ensure!(recovered_address == ethereum_address,
|
||||
Error::<T, I>::InvalidEthereumAddress);
|
||||
|
||||
Self::do_claim(who, ethereum_address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
|
||||
let mut r = Vec::with_capacity(data.len() * 2);
|
||||
let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });
|
||||
for &b in data.iter() {
|
||||
push_nibble(b / 16);
|
||||
push_nibble(b % 16);
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
fn ethereum_signable_message(what: &[u8]) -> Vec<u8> {
|
||||
let prefix = T::Prefix::get();
|
||||
let mut l = prefix.len() + what.len();
|
||||
let mut rev = Vec::new();
|
||||
while l > 0 {
|
||||
rev.push(b'0' + (l % 10) as u8);
|
||||
l /= 10;
|
||||
}
|
||||
let mut v = b"\x19Ethereum Signed Message:\n".to_vec();
|
||||
v.extend(rev.into_iter().rev());
|
||||
v.extend_from_slice(prefix);
|
||||
v.extend_from_slice(what);
|
||||
v
|
||||
}
|
||||
|
||||
fn recover_ethereum_address(s: &EcdsaSignature, what: &[u8]) -> Option<EthereumAddress> {
|
||||
let msg = keccak_256(&Self::ethereum_signable_message(what));
|
||||
let mut res = EthereumAddress::default();
|
||||
res.0
|
||||
.copy_from_slice(&keccak_256(&secp256k1_ecdsa_recover(&s.0, &msg).ok()?[..])[12..]);
|
||||
Some(res)
|
||||
}
|
||||
|
||||
fn into_account_id(address: EthereumAddress) -> Result<T::AccountId, codec::Error> {
|
||||
let mut data = [0u8; 32];
|
||||
data[0..4].copy_from_slice(b"evm:");
|
||||
data[4..24].copy_from_slice(&address.0[..]);
|
||||
|
||||
let hash = sp_core::keccak_256(&data);
|
||||
T::AccountId::decode(&mut &hash[..])
|
||||
}
|
||||
|
||||
fn do_claim(receiver: T::AccountId, ethereum_address: EthereumAddress) -> DispatchResult {
|
||||
let donor = Self::into_account_id(ethereum_address).ok()
|
||||
.ok_or(Error::<T, I>::AddressDecodingFailed)?;
|
||||
|
||||
let balance_due = CurrencyOf::<T, I>::free_balance(&donor);
|
||||
ensure!(balance_due >= CurrencyOf::<T, I>::minimum_balance(),
|
||||
Error::<T, I>::NoBalanceToClaim);
|
||||
|
||||
let new_total = Total::<T, I>::get()
|
||||
.checked_sub(&balance_due)
|
||||
.ok_or(Error::<T, I>::PotUnderflow)?;
|
||||
|
||||
CurrencyOf::<T, I>::transfer(
|
||||
&donor,
|
||||
&receiver,
|
||||
balance_due,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
)?;
|
||||
|
||||
let max_amount = T::MaximumWithdrawAmount::get();
|
||||
if balance_due > max_amount {
|
||||
let vesting_balance = balance_due
|
||||
.checked_sub(&max_amount)
|
||||
.ok_or(Error::<T, I>::ArithmeticError)?;
|
||||
|
||||
let vesting_blocks = T::VestingBlocks::get();
|
||||
let per_block_balance = vesting_balance
|
||||
.checked_div(&vesting_blocks.into())
|
||||
.ok_or(Error::<T, I>::ArithmeticError)?;
|
||||
|
||||
T::VestingSchedule::add_vesting_schedule(
|
||||
&receiver,
|
||||
vesting_balance,
|
||||
per_block_balance,
|
||||
T::BlockNumberProvider::current_block_number(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let rank = if let Some(rank) = <pallet_ranked_collective::Pallet::<T, I> as RankedMembers>::rank_of(&donor) {
|
||||
pallet_ranked_collective::Pallet::<T, I>::do_remove_member_from_rank(&donor, rank)?;
|
||||
let new_rank = match <pallet_ranked_collective::Pallet::<T, I> as RankedMembers>::rank_of(&receiver) {
|
||||
Some(current_rank) if current_rank >= rank => current_rank,
|
||||
Some(current_rank) if current_rank < rank => {
|
||||
for _ in 0..rank - current_rank {
|
||||
pallet_ranked_collective::Pallet::<T, I>::do_promote_member(receiver.clone(), None, false)?;
|
||||
}
|
||||
rank
|
||||
},
|
||||
_ => {
|
||||
pallet_ranked_collective::Pallet::<T, I>::do_add_member_to_rank(receiver.clone(), rank, false)?;
|
||||
rank
|
||||
},
|
||||
};
|
||||
<T as pallet::Config<I>>::MemberSwappedHandler::swapped(&donor, &receiver, new_rank);
|
||||
Some(new_rank)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Total::<T, I>::put(new_total);
|
||||
Self::deposit_event(Event::<T, I>::Claimed {
|
||||
receiver,
|
||||
donor,
|
||||
amount: balance_due,
|
||||
rank,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
221
pallets/claims/src/mock.rs
Normal file
221
pallets/claims/src/mock.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use super::*;
|
||||
|
||||
pub use crate as ghost_claims;
|
||||
use frame_support::{
|
||||
parameter_types, derive_impl,
|
||||
traits::{PollStatus, Polling, WithdrawReasons, ConstU16, ConstU64},
|
||||
};
|
||||
use frame_system::EnsureRootWithSuccess;
|
||||
use sp_runtime::{BuildStorage, traits::Convert};
|
||||
pub use pallet_ranked_collective::{TallyOf, Rank};
|
||||
|
||||
pub mod eth_keys {
|
||||
use crate::{
|
||||
mock::Test,
|
||||
EcdsaSignature, EthereumAddress,
|
||||
};
|
||||
use hex_literal::hex;
|
||||
use codec::Encode;
|
||||
|
||||
pub fn total_claims() -> u64 { 10 + 100 + 1000 }
|
||||
pub fn alice_account_id() -> <Test as frame_system::Config>::AccountId { 69 }
|
||||
pub fn bob_account_id() -> <Test as frame_system::Config>::AccountId { 1337 }
|
||||
|
||||
pub fn first_eth_public_known() -> EthereumAddress { EthereumAddress(hex!("1A69d2D5568D1878023EeB121a73d33B9116A760")) }
|
||||
pub fn second_eth_public_known() -> EthereumAddress { EthereumAddress(hex!("2f86cfBED3fbc1eCf2989B9aE5fc019a837A9C12")) }
|
||||
pub fn third_eth_public_known() -> EthereumAddress { EthereumAddress(hex!("e83f67361Ac74D42A48E2DAfb6706eb047D8218D")) }
|
||||
pub fn fourth_eth_public_known() -> EthereumAddress { EthereumAddress(hex!("827ee4ad9b259b6fa1390ed60921508c78befd63")) }
|
||||
|
||||
fn first_eth_private_key() -> libsecp256k1::SecretKey { libsecp256k1::SecretKey::parse(&hex!("01c928771aea942a1e7ac06adf2b73dfbc9a25d9eaa516e3673116af7f345198")).unwrap() }
|
||||
fn second_eth_private_key() -> libsecp256k1::SecretKey { libsecp256k1::SecretKey::parse(&hex!("b19a435901872f817185f7234a1484eae837613f9d10cf21927a23c2d8cb9139")).unwrap() }
|
||||
fn third_eth_private_key() -> libsecp256k1::SecretKey { libsecp256k1::SecretKey::parse(&hex!("d3baf57b74d65719b2dc33f5a464176022d0cc5edbca002234229f3e733875fc")).unwrap() }
|
||||
fn fourth_eth_private_key() -> libsecp256k1::SecretKey { libsecp256k1::SecretKey::parse(&hex!("c4683d566436af6b58b4a59c8f501319226e85b21869bf93d5eeb4596d4791d4")).unwrap() }
|
||||
fn wrong_eth_private_key() -> libsecp256k1::SecretKey { libsecp256k1::SecretKey::parse(&hex!("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")).unwrap() }
|
||||
|
||||
pub fn first_eth_public_key() -> EthereumAddress { crate::secp_utils::eth(&first_eth_private_key()) }
|
||||
pub fn second_eth_public_key() -> EthereumAddress { crate::secp_utils::eth(&second_eth_private_key()) }
|
||||
pub fn third_eth_public_key() -> EthereumAddress { crate::secp_utils::eth(&third_eth_private_key()) }
|
||||
pub fn fourth_eth_public_key() -> EthereumAddress { crate::secp_utils::eth(&fourth_eth_private_key()) }
|
||||
|
||||
pub fn first_account_id() -> <Test as frame_system::Config>::AccountId { crate::secp_utils::into_account_id::<Test, ()>(first_eth_public_key()) }
|
||||
pub fn second_account_id() -> <Test as frame_system::Config>::AccountId { crate::secp_utils::into_account_id::<Test, ()>(second_eth_public_key()) }
|
||||
pub fn third_account_id() -> <Test as frame_system::Config>::AccountId { crate::secp_utils::into_account_id::<Test, ()>(third_eth_public_key()) }
|
||||
pub fn fourth_account_id() -> <Test as frame_system::Config>::AccountId { crate::secp_utils::into_account_id::<Test, ()>(fourth_eth_public_key()) }
|
||||
|
||||
pub fn first_signature() -> EcdsaSignature { crate::secp_utils::sig::<Test, ()>(&first_eth_private_key(), &alice_account_id().encode()) }
|
||||
pub fn second_signature() -> EcdsaSignature { crate::secp_utils::sig::<Test, ()>(&second_eth_private_key(), &alice_account_id().encode()) }
|
||||
pub fn third_signature() -> EcdsaSignature { crate::secp_utils::sig::<Test, ()>(&third_eth_private_key(), &alice_account_id().encode()) }
|
||||
pub fn fourth_signature() -> EcdsaSignature { crate::secp_utils::sig::<Test, ()>(&fourth_eth_private_key(), &bob_account_id().encode()) }
|
||||
pub fn wrong_signature() -> EcdsaSignature { crate::secp_utils::sig::<Test, ()>(&wrong_eth_private_key(), &alice_account_id().encode()) }
|
||||
}
|
||||
|
||||
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
|
||||
impl frame_system::Config for Test {
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type AccountData = pallet_balances::AccountData<u64>;
|
||||
type MaxConsumers = frame_support::traits::ConstU32<16>;
|
||||
}
|
||||
|
||||
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pallet_balances::Config for Test {
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
|
||||
parameter_types! {
|
||||
pub const MinVestedTransfer: u64 = 1;
|
||||
pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons =
|
||||
WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE);
|
||||
}
|
||||
|
||||
impl pallet_vesting::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type BlockNumberToBalance = sp_runtime::traits::ConvertInto;
|
||||
type MinVestedTransfer = MinVestedTransfer;
|
||||
type WeightInfo = ();
|
||||
type UnvestedFundsAllowedWithdrawReasons =
|
||||
UnvestedFundsAllowedWithdrawReasons;
|
||||
|
||||
type BlockNumberProvider = System;
|
||||
const MAX_VESTING_SCHEDULES: u32 = 28;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub MinRankOfClassDelta: Rank = 1;
|
||||
}
|
||||
|
||||
pub struct MinRankOfClass<Delta>(sp_std::marker::PhantomData<Delta>);
|
||||
impl<Delta: Get<Rank>> Convert<Class, Rank> for MinRankOfClass<Delta> {
|
||||
fn convert(a: Class) -> Rank {
|
||||
a.saturating_sub(Delta::get())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestPolls;
|
||||
impl Polling<TallyOf<Test>> for TestPolls {
|
||||
type Index = u8;
|
||||
type Votes = u32;
|
||||
type Moment = u64;
|
||||
type Class = Class;
|
||||
|
||||
fn classes() -> Vec<Self::Class> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_ongoing(_index: u8) -> Option<(TallyOf<Test>, Self::Class)> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn access_poll<R>(
|
||||
_index: Self::Index,
|
||||
_f: impl FnOnce(PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>) -> R,
|
||||
) -> R {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn try_access_poll<R>(
|
||||
_index: Self::Index,
|
||||
_f: impl FnOnce(
|
||||
PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>,
|
||||
) -> Result<R, DispatchError>,
|
||||
) -> Result<R, DispatchError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn create_ongoing(_class: Self::Class) -> Result<Self::Index, ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn end_ongoing(_index: Self::Index, _approved: bool) -> Result<(), ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_ranked_collective::Config for Test {
|
||||
type WeightInfo = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
|
||||
type AddOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
|
||||
type RemoveOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
|
||||
type PromoteOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
|
||||
type DemoteOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
|
||||
type ExchangeOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
|
||||
|
||||
type Polls = TestPolls;
|
||||
type MemberSwappedHandler = ();
|
||||
type MinRankOfClass = MinRankOfClass<MinRankOfClassDelta>;
|
||||
type VoteWeight = pallet_ranked_collective::Geometric;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type BenchmarkSetup = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub Prefix: &'static [u8] = b"AccountId:";
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type VestingSchedule = Vesting;
|
||||
type BlockNumberProvider = System;
|
||||
type MemberSwappedHandler = ();
|
||||
|
||||
type Prefix = Prefix;
|
||||
type MaximumWithdrawAmount = ConstU64<200>;
|
||||
type VestingBlocks = ConstU32<80>;
|
||||
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
type Class = Rank;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: frame_system,
|
||||
Balances: pallet_balances,
|
||||
Vesting: pallet_vesting,
|
||||
Club: pallet_ranked_collective,
|
||||
Claims: ghost_claims,
|
||||
}
|
||||
);
|
||||
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::<Test>::default()
|
||||
.build_storage()
|
||||
.unwrap();
|
||||
|
||||
pallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![
|
||||
(crate::mock::eth_keys::first_account_id(), 10),
|
||||
(crate::mock::eth_keys::second_account_id(), 100),
|
||||
(crate::mock::eth_keys::third_account_id(), 1000),
|
||||
],
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
ghost_claims::GenesisConfig::<Test> {
|
||||
total: crate::mock::eth_keys::total_claims(),
|
||||
members_and_ranks: vec![
|
||||
(crate::mock::eth_keys::second_account_id(), 1),
|
||||
(crate::mock::eth_keys::third_account_id(), 3),
|
||||
],
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
});
|
||||
ext
|
||||
}
|
||||
37
pallets/claims/src/secp_utils.rs
Normal file
37
pallets/claims/src/secp_utils.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
#![cfg(any(test, feature = "runtime-benchmarks"))]
|
||||
|
||||
use crate::{keccak_256, Config, EcdsaSignature, EthereumAddress};
|
||||
|
||||
pub fn public(secret: &libsecp256k1::SecretKey) -> libsecp256k1::PublicKey {
|
||||
libsecp256k1::PublicKey::from_secret_key(secret)
|
||||
}
|
||||
|
||||
pub fn eth(secret: &libsecp256k1::SecretKey) -> EthereumAddress {
|
||||
let mut res = EthereumAddress::default();
|
||||
res.0.copy_from_slice(&keccak_256(&public(secret).serialize()[1..65])[12..]);
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn into_account_id<T: Config<I>, I: 'static>(address: EthereumAddress) -> T::AccountId {
|
||||
super::Pallet::<T, I>::into_account_id(address).unwrap()
|
||||
}
|
||||
|
||||
pub fn sig<T: Config<I>, I: 'static>(
|
||||
secret: &libsecp256k1::SecretKey,
|
||||
what: &[u8],
|
||||
) -> EcdsaSignature {
|
||||
let msg = keccak_256(&super::Pallet::<T, I>::ethereum_signable_message(
|
||||
&crate::to_ascii_hex(what)[..],
|
||||
));
|
||||
|
||||
let (sig, recovery_id) = libsecp256k1::sign(
|
||||
&libsecp256k1::Message::parse(&msg),
|
||||
secret,
|
||||
);
|
||||
|
||||
let mut r = [0u8; 65];
|
||||
r[0..64].copy_from_slice(&sig.serialize()[..]);
|
||||
r[64] = recovery_id.serialize();
|
||||
EcdsaSignature(r)
|
||||
}
|
||||
274
pallets/claims/src/tests.rs
Normal file
274
pallets/claims/src/tests.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use mock::{
|
||||
new_test_ext, ghost_claims,
|
||||
Test, System, Balances, Club, Vesting, Claims, RuntimeOrigin, RuntimeEvent,
|
||||
eth_keys::{
|
||||
alice_account_id, total_claims, first_eth_public_known,
|
||||
first_eth_public_key, first_account_id, first_signature,
|
||||
second_eth_public_known, second_eth_public_key, second_account_id,
|
||||
second_signature, third_eth_public_known, third_eth_public_key,
|
||||
third_account_id, third_signature, fourth_eth_public_known,
|
||||
fourth_eth_public_key, fourth_account_id, fourth_signature,
|
||||
wrong_signature, bob_account_id,
|
||||
}
|
||||
};
|
||||
use hex_literal::hex;
|
||||
use frame_support::{assert_err, assert_ok};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn serde_works() {
|
||||
let x = EthereumAddress(hex!["0123456789abcdef0123456789abcdef01234567"]);
|
||||
let y = serde_json::to_string(&x).unwrap();
|
||||
assert_eq!(y, "\"0x0123456789abcdef0123456789abcdef01234567\"");
|
||||
let z: EthereumAddress = serde_json::from_str(&y).unwrap();
|
||||
assert_eq!(x, z);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_eth_public_accounts_are_correct() {
|
||||
assert_eq!(first_eth_public_key(), first_eth_public_known());
|
||||
assert_eq!(second_eth_public_key(), second_eth_public_known());
|
||||
assert_eq!(third_eth_public_key(), third_eth_public_known());
|
||||
assert_eq!(fourth_eth_public_key(), fourth_eth_public_known());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_setup_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), None);
|
||||
assert_eq!(Club::rank_of(&first_account_id()), None);
|
||||
assert_eq!(Club::rank_of(&second_account_id()), Some(1));
|
||||
assert_eq!(Club::rank_of(&third_account_id()), Some(3));
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_claiming_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
first_eth_public_key(),
|
||||
first_signature()));
|
||||
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 10);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), None);
|
||||
assert_eq!(Club::rank_of(&first_account_id()), None);
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims() - 10);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_claiming_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
second_eth_public_key(),
|
||||
second_signature(),
|
||||
));
|
||||
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 100);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), Some(1));
|
||||
assert_eq!(Club::rank_of(&second_account_id()), None);
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims() - 100);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_claiming_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
third_eth_public_key(),
|
||||
third_signature(),
|
||||
));
|
||||
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 200);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 0);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), Some(3));
|
||||
assert_eq!(Club::rank_of(&third_account_id()), None);
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), Some(800));
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims() - 1000);
|
||||
assert_ok!(Balances::transfer_allow_death(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
bob_account_id(),
|
||||
200));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_accounts_claiming_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
first_eth_public_key(),
|
||||
first_signature(),
|
||||
));
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
second_eth_public_key(),
|
||||
second_signature(),
|
||||
));
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
third_eth_public_key(),
|
||||
third_signature(),
|
||||
));
|
||||
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 310);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 0);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), Some(3));
|
||||
assert_eq!(Club::rank_of(&third_account_id()), None);
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), Some(800));
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), 0);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_accounts_reverese_claiming_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
third_eth_public_key(),
|
||||
third_signature(),
|
||||
));
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
second_eth_public_key(),
|
||||
second_signature(),
|
||||
));
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
first_eth_public_key(),
|
||||
first_signature(),
|
||||
));
|
||||
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 310);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 0);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), Some(3));
|
||||
assert_eq!(Club::rank_of(&third_account_id()), None);
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), Some(800));
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), 0);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_claim_with_bad_signature() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_err!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
first_eth_public_key(),
|
||||
wrong_signature()),
|
||||
crate::Error::<Test>::InvalidEthereumAddress);
|
||||
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), None);
|
||||
assert_eq!(Club::rank_of(&first_account_id()), None);
|
||||
assert_eq!(Club::rank_of(&second_account_id()), Some(1));
|
||||
assert_eq!(Club::rank_of(&third_account_id()), Some(3));
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_claim_with_wrong_address() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_err!(Claims::claim(
|
||||
RuntimeOrigin::signed(bob_account_id()),
|
||||
first_eth_public_key(),
|
||||
first_signature()),
|
||||
crate::Error::<Test>::InvalidEthereumAddress);
|
||||
|
||||
assert_eq!(Balances::usable_balance(&bob_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&alice_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
|
||||
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
|
||||
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
|
||||
|
||||
assert_eq!(Club::rank_of(&alice_account_id()), None);
|
||||
assert_eq!(Club::rank_of(&first_account_id()), None);
|
||||
assert_eq!(Club::rank_of(&second_account_id()), Some(1));
|
||||
assert_eq!(Club::rank_of(&third_account_id()), Some(3));
|
||||
|
||||
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_claim_nothing() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_err!(Claims::claim(
|
||||
RuntimeOrigin::signed(bob_account_id()),
|
||||
fourth_eth_public_key(),
|
||||
fourth_signature()),
|
||||
crate::Error::<Test>::NoBalanceToClaim);
|
||||
assert_eq!(Balances::usable_balance(&bob_account_id()), 0);
|
||||
assert_eq!(Balances::usable_balance(&fourth_account_id()), 0);
|
||||
assert_eq!(Vesting::vesting_balance(&bob_account_id()), None);
|
||||
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_emitted_during_claim() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = third_account_id();
|
||||
let amount = Balances::usable_balance(&account);
|
||||
let rank = Club::rank_of(&account);
|
||||
|
||||
System::reset_events();
|
||||
assert_eq!(System::event_count(), 0);
|
||||
assert_ok!(Claims::claim(
|
||||
RuntimeOrigin::signed(alice_account_id()),
|
||||
third_eth_public_key(),
|
||||
third_signature(),
|
||||
));
|
||||
System::assert_has_event(RuntimeEvent::Claims(
|
||||
crate::Event::Claimed {
|
||||
receiver: alice_account_id(),
|
||||
donor: account,
|
||||
amount,
|
||||
rank,
|
||||
}));
|
||||
})
|
||||
}
|
||||
9
pallets/claims/src/weights.rs
Normal file
9
pallets/claims/src/weights.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use frame_support::weights::Weight;
|
||||
|
||||
pub trait WeightInfo {
|
||||
fn claim() -> Weight;
|
||||
}
|
||||
|
||||
impl WeightInfo for () {
|
||||
fn claim() -> Weight { Weight::zero() }
|
||||
}
|
||||
Reference in New Issue
Block a user