1#![cfg_attr(not(feature = "std"), no_std)]
55#![deny(missing_docs)]
56
57extern crate alloc;
58
59use alloc::{collections::BTreeMap, format};
60use codec::{Decode, DecodeWithMemTracking, Encode};
61use frame_support::{
62 sp_runtime::traits::AccountIdConversion,
63 traits::{fungible::Mutate, tokens::Preservation, Get},
64};
65use ismp::{
66 dispatcher::{DispatchRequest, FeeMetadata, IsmpDispatcher},
67 host::StateMachine,
68 module::IsmpModule,
69 router::{PostRequest, PostResponse, Response, Timeout},
70};
71pub use pallet::*;
72use pallet_ismp::RELAYER_FEE_ACCOUNT;
73use polkadot_sdk::{sp_runtime::Weight, *};
74use primitive_types::H256;
75
76pub mod child_trie;
77
78#[derive(
80 Debug,
81 Clone,
82 Encode,
83 Decode,
84 DecodeWithMemTracking,
85 scale_info::TypeInfo,
86 PartialEq,
87 Eq,
88 Default,
89)]
90pub struct SubstrateHostParams<B> {
91 pub default_per_byte_fee: B,
93 pub per_byte_fees: BTreeMap<StateMachine, B>,
95 pub asset_registration_fee: B,
97}
98
99#[derive(
101 Debug, Clone, Encode, Decode, DecodeWithMemTracking, scale_info::TypeInfo, PartialEq, Eq,
102)]
103pub enum VersionedHostParams<Balance> {
104 V1(SubstrateHostParams<Balance>),
106}
107
108impl<Balance: Default> Default for VersionedHostParams<Balance> {
109 fn default() -> Self {
110 VersionedHostParams::V1(Default::default())
111 }
112}
113
114#[frame_support::pallet]
115pub mod pallet {
116 use super::*;
117 use frame_support::{pallet_prelude::*, PalletId};
118
119 pub const PALLET_HYPERBRIDGE_ID: &'static [u8] = b"HYPR-FEE";
121
122 pub const PALLET_HYPERBRIDGE: PalletId = PalletId(*b"HYPR-FEE");
124
125 #[pallet::config]
126 pub trait Config: polkadot_sdk::frame_system::Config + pallet_ismp::Config {
127 type IsmpHost: IsmpDispatcher<Account = Self::AccountId, Balance = Self::Balance> + Default;
129 }
130
131 #[pallet::pallet]
132 #[pallet::without_storage_info]
133 pub struct Pallet<T>(_);
134
135 #[pallet::storage]
137 #[pallet::getter(fn host_params)]
138 pub type HostParams<T> =
139 StorageValue<_, VersionedHostParams<<T as pallet_ismp::Config>::Balance>, ValueQuery>;
140
141 #[pallet::event]
142 #[pallet::generate_deposit(pub(super) fn deposit_event)]
143 pub enum Event<T: Config> {
144 HostParamsUpdated {
146 old: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
148 new: VersionedHostParams<<T as pallet_ismp::Config>::Balance>,
150 },
151 RelayerFeeWithdrawn {
153 amount: <T as pallet_ismp::Config>::Balance,
155 account: T::AccountId,
157 },
158 }
159
160 #[pallet::error]
162 pub enum Error<T> {}
163
164 impl<T> Default for Pallet<T> {
168 fn default() -> Self {
169 Self(PhantomData)
170 }
171 }
172}
173
174impl<T> IsmpDispatcher for Pallet<T>
180where
181 T: Config,
182 T::Balance: Into<u128> + From<u128>,
183{
184 type Account = T::AccountId;
185 type Balance = T::Balance;
186
187 fn dispatch_request(
188 &self,
189 request: DispatchRequest,
190 fee: FeeMetadata<Self::Account, Self::Balance>,
191 ) -> Result<H256, anyhow::Error> {
192 let fees = match request {
193 DispatchRequest::Post(ref post) => {
194 let VersionedHostParams::V1(params) = Self::host_params();
195 let per_byte_fee: u128 =
196 (*params.per_byte_fees.get(&post.dest).unwrap_or(¶ms.default_per_byte_fee))
197 .into();
198 let fees = if post.body.len() < 32 {
200 per_byte_fee * 32u128
201 } else {
202 per_byte_fee * post.body.len() as u128
203 };
204
205 if fees != 0 {
207 T::Currency::transfer(
208 &fee.payer,
209 &RELAYER_FEE_ACCOUNT.into_account_truncating(),
210 fees.into(),
211 Preservation::Expendable,
212 )
213 .map_err(|err| {
214 ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
215 })?;
216 }
217
218 fees
219 },
220 DispatchRequest::Get(_) => Default::default(),
221 };
222
223 let host = <T as Config>::IsmpHost::default();
224 let commitment = host.dispatch_request(request, fee)?;
225
226 child_trie::RequestPayments::insert(commitment, fees);
228
229 Ok(commitment)
230 }
231
232 fn dispatch_response(
233 &self,
234 response: PostResponse,
235 fee: FeeMetadata<Self::Account, Self::Balance>,
236 ) -> Result<H256, anyhow::Error> {
237 let VersionedHostParams::V1(params) = Self::host_params();
239 let per_byte_fee: u128 = (*params
240 .per_byte_fees
241 .get(&response.dest_chain())
242 .unwrap_or(¶ms.default_per_byte_fee))
243 .into();
244 let fees = if response.response.len() < 32 {
246 per_byte_fee * 32u128
247 } else {
248 per_byte_fee * response.response.len() as u128
249 };
250
251 if fees != 0 {
252 T::Currency::transfer(
253 &fee.payer,
254 &RELAYER_FEE_ACCOUNT.into_account_truncating(),
255 fees.into(),
256 Preservation::Expendable,
257 )
258 .map_err(|err| {
259 ismp::Error::Custom(format!("Error withdrawing request fees: {err:?}"))
260 })?;
261 }
262
263 let host = <T as Config>::IsmpHost::default();
264 let commitment = host.dispatch_response(response, fee)?;
265
266 child_trie::ResponsePayments::insert(commitment, fees);
268
269 Ok(commitment)
270 }
271}
272
273#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
275pub struct WithdrawalRequest<Account, Amount> {
276 pub amount: Amount,
278 pub account: Account,
280}
281
282#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
285pub enum Message<Account, Balance> {
286 #[codec(index = 0)]
288 UpdateHostParams(VersionedHostParams<Balance>),
289 #[codec(index = 2)]
291 WithdrawRelayerFees(WithdrawalRequest<Account, Balance>),
292}
293
294impl<T> IsmpModule for Pallet<T>
295where
296 T: Config,
297 T::Balance: Into<u128> + From<u128>,
298{
299 fn on_accept(&self, request: PostRequest) -> Result<Weight, anyhow::Error> {
300 let source = request.source;
302 if Some(source) != T::Coprocessor::get() {
303 Err(ismp::Error::Custom(format!("Invalid request source: {source}")))?
304 }
305
306 let message =
307 Message::<T::AccountId, T::Balance>::decode(&mut &request.body[..]).map_err(|err| {
308 ismp::Error::Custom(format!("Failed to decode per-byte fee: {err:?}"))
309 })?;
310
311 let weight = match message {
312 Message::UpdateHostParams(new) => {
313 let old = HostParams::<T>::get();
314 HostParams::<T>::put(new.clone());
315 Self::deposit_event(Event::<T>::HostParamsUpdated { old, new });
316 T::DbWeight::get().reads_writes(0, 0)
317 },
318 Message::WithdrawRelayerFees(WithdrawalRequest { account, amount }) => {
319 T::Currency::transfer(
320 &RELAYER_FEE_ACCOUNT.into_account_truncating(),
321 &account,
322 amount,
323 Preservation::Expendable,
324 )
325 .map_err(|err| {
326 ismp::Error::Custom(format!("Error withdrawing protocol fees: {err:?}"))
327 })?;
328
329 Self::deposit_event(Event::<T>::RelayerFeeWithdrawn { account, amount });
330 T::DbWeight::get().reads_writes(0, 0)
331 },
332 };
333
334 Ok(weight)
335 }
336
337 fn on_response(&self, _response: Response) -> Result<Weight, anyhow::Error> {
338 Err(ismp::Error::CannotHandleMessage.into())
340 }
341
342 fn on_timeout(&self, _request: Timeout) -> Result<Weight, anyhow::Error> {
343 Err(ismp::Error::CannotHandleMessage.into())
345 }
346}