1#![doc = include_str!("../README.md")]
9#![allow(clippy::expect_used)]
11#![cfg_attr(not(feature = "std"), no_std)]
12#![deny(
14 rustdoc::broken_intra_doc_links,
15 rustdoc::missing_crate_level_docs,
16 rustdoc::invalid_codeblock_attributes,
17 missing_docs
18)]
19#![allow(deprecated)]
21use common_primitives::node::EIP712Encode;
22use common_runtime::{
23 extensions::check_nonce::{prepare_nonce, validate_nonce},
24 signature::check_signature,
25};
26use frame_support::{
27 dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo, RawOrigin},
28 pallet_prelude::*,
29 traits::Contains,
30};
31use frame_system::pallet_prelude::*;
32use pallet_transaction_payment::OnChargeTransaction;
33use sp_runtime::{
34 generic::Era,
35 traits::{AsTransactionAuthorizedOrigin, Convert, Dispatchable, TxBaseImplication, Zero},
36 transaction_validity::{TransactionValidity, TransactionValidityError},
37 AccountId32, MultiSignature,
38};
39extern crate alloc;
40use alloc::vec;
41
42pub(crate) type OnChargeTransactionOf<T> =
44 <T as pallet_transaction_payment::Config>::OnChargeTransaction;
45
46pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
48
49#[cfg(any(feature = "runtime-benchmarks", test))]
50mod test_common;
51
52#[cfg(test)]
53mod mock;
54#[cfg(test)]
55mod tests;
56#[cfg(test)]
57mod tests_v2;
58
59pub mod weights;
60pub use weights::*;
61
62#[cfg(feature = "runtime-benchmarks")]
63use frame_support::traits::tokens::fungible::Mutate;
64use frame_system::CheckWeight;
65use sp_runtime::traits::{DispatchTransaction, TransactionExtension};
66
67#[cfg(feature = "runtime-benchmarks")]
68mod benchmarking;
69
70pub mod types;
72pub use types::*;
73
74pub use module::*;
75
76#[frame_support::pallet]
77pub mod module {
78
79 use super::*;
80
81 pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
83
84 #[pallet::config]
85 pub trait Config:
86 frame_system::Config + pallet_transaction_payment::Config + Send + Sync
87 {
88 #[allow(deprecated)]
90 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
91
92 type RuntimeCall: Parameter
94 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
95 + GetDispatchInfo
96 + From<frame_system::Call<Self>>
97 + IsType<<Self as frame_system::Config>::RuntimeCall>
98 + From<Call<Self>>;
99
100 type WeightInfo: WeightInfo;
102
103 type ConvertIntoAccountId32: Convert<Self::AccountId, AccountId32>;
105
106 type PasskeyCallFilter: Contains<<Self as Config>::RuntimeCall>;
108
109 #[cfg(feature = "runtime-benchmarks")]
111 type Currency: Mutate<Self::AccountId>;
112 }
113
114 #[pallet::error]
115 pub enum Error<T> {
116 InvalidAccountSignature,
118 }
119
120 #[pallet::event]
121 #[pallet::generate_deposit(fn deposit_event)]
122 pub enum Event<T: Config> {
123 TransactionExecutionSuccess {
125 account_id: T::AccountId,
127 },
128 }
129
130 #[pallet::pallet]
131 #[pallet::storage_version(STORAGE_VERSION)]
132 pub struct Pallet<T>(_);
133
134 #[pallet::call]
135 impl<T: Config> Pallet<T> {
136 #[deprecated(since = "1.15.2", note = "Use proxy_v2 instead")]
140 #[pallet::call_index(0)]
141 #[pallet::weight({
142 let dispatch_info = payload.passkey_call.call.get_dispatch_info();
143 let overhead = <T as Config>::WeightInfo::pre_dispatch();
144 let total = overhead.saturating_add(dispatch_info.call_weight);
145 (total, dispatch_info.class)
146 })]
147 #[allow(clippy::useless_conversion)]
148 pub fn proxy(
149 origin: OriginFor<T>,
150 payload: PasskeyPayload<T>,
151 ) -> DispatchResultWithPostInfo {
152 Self::proxy_v2(origin, payload.into())
153 }
154
155 #[pallet::call_index(1)]
159 #[pallet::weight({
160 let dispatch_info = payload.passkey_call.call.get_dispatch_info();
161 let overhead = <T as Config>::WeightInfo::pre_dispatch();
162 let total = overhead.saturating_add(dispatch_info.call_weight);
163 (total, dispatch_info.class)
164 })]
165 #[allow(clippy::useless_conversion)]
166 pub fn proxy_v2(
167 origin: OriginFor<T>,
168 payload: PasskeyPayloadV2<T>,
169 ) -> DispatchResultWithPostInfo {
170 ensure_none(origin)?;
171 let transaction_account_id = payload.passkey_call.account_id.clone();
172 let main_origin = T::RuntimeOrigin::from(frame_system::RawOrigin::Signed(
173 transaction_account_id.clone(),
174 ));
175 let result = payload.passkey_call.call.dispatch(main_origin);
176 if let Ok(_inner) = result {
177 Self::deposit_event(Event::TransactionExecutionSuccess {
179 account_id: transaction_account_id,
180 });
181 }
182 result
183 }
184 }
185
186 #[pallet::validate_unsigned]
187 impl<T: Config> ValidateUnsigned for Pallet<T>
188 where
189 BalanceOf<T>: Send + Sync + From<u64>,
190 <T as frame_system::Config>::RuntimeCall:
191 From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
192 <T as frame_system::Config>::RuntimeOrigin: AsTransactionAuthorizedOrigin,
193 {
194 type Call = Call<T>;
195
196 fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
199 let valid_tx = ValidTransaction::default();
200 let (payload, is_legacy_call) = Self::filter_valid_calls(call)?;
201
202 let frame_system_validity =
203 FrameSystemChecks(payload.passkey_call.account_id.clone(), call.clone())
204 .validate()?;
205 let nonce_validity = PasskeyNonceCheck::new(payload.passkey_call.clone()).validate()?;
206 let weight_validity =
207 PasskeyWeightCheck::new(payload.passkey_call.account_id.clone(), call.clone())
208 .validate()?;
209 let tx_payment_validity = ChargeTransactionPayment::<T>(
210 payload.passkey_call.account_id.clone(),
211 call.clone(),
212 )
213 .validate()?;
214 let signature_validity =
216 PasskeySignatureCheck::new(payload.clone(), is_legacy_call).validate()?;
217
218 let valid_tx = valid_tx
219 .combine_with(frame_system_validity)
220 .combine_with(nonce_validity)
221 .combine_with(weight_validity)
222 .combine_with(tx_payment_validity)
223 .combine_with(signature_validity);
224 Ok(valid_tx)
225 }
226
227 fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
230 let (payload, is_legacy_call) = Self::filter_valid_calls(call)?;
231 FrameSystemChecks(payload.passkey_call.account_id.clone(), call.clone())
232 .pre_dispatch()?;
233 PasskeyNonceCheck::new(payload.passkey_call.clone()).pre_dispatch()?;
234 PasskeyWeightCheck::new(payload.passkey_call.account_id.clone(), call.clone())
235 .pre_dispatch()?;
236 ChargeTransactionPayment::<T>(payload.passkey_call.account_id.clone(), call.clone())
237 .pre_dispatch()?;
238 PasskeySignatureCheck::new(payload.clone(), is_legacy_call).pre_dispatch()
240 }
241 }
242}
243
244impl<T: Config> Pallet<T>
245where
246 BalanceOf<T>: Send + Sync + From<u64>,
247 <T as frame_system::Config>::RuntimeCall:
248 From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
249{
250 fn filter_valid_calls(
253 call: &Call<T>,
254 ) -> Result<(PasskeyPayloadV2<T>, bool), TransactionValidityError> {
255 match call {
256 Call::proxy { payload }
257 if T::PasskeyCallFilter::contains(&payload.clone().passkey_call.call) =>
258 Ok((payload.clone().into(), true)),
259 Call::proxy_v2 { payload }
260 if T::PasskeyCallFilter::contains(&payload.clone().passkey_call.call) =>
261 Ok((payload.clone(), false)),
262 _ => Err(InvalidTransaction::Call.into()),
263 }
264 }
265}
266
267#[derive(Encode, Decode, Clone, TypeInfo)]
269#[scale_info(skip_type_params(T))]
270struct PasskeyNonceCheck<T: Config>(pub PasskeyCallV2<T>);
271
272impl<T: Config> PasskeyNonceCheck<T>
273where
274 <T as frame_system::Config>::RuntimeCall: Dispatchable<Info = DispatchInfo>,
275{
276 pub fn new(passkey_call: PasskeyCallV2<T>) -> Self {
277 Self(passkey_call)
278 }
279
280 pub fn validate(&self) -> TransactionValidity {
281 let who = self.0.account_id.clone();
282 let nonce = self.0.account_nonce;
283 let (nonce_validity, _) = validate_nonce::<T>(&who, nonce)?;
284 Ok(nonce_validity)
285 }
286
287 pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
288 let who = self.0.account_id.clone();
289 let nonce = self.0.account_nonce;
290 let some_call: &<T as Config>::RuntimeCall = &self.0.call;
291 let info = &some_call.get_dispatch_info();
292 prepare_nonce::<T>(&who, nonce, info.pays_fee)?;
293 Ok(())
294 }
295}
296
297#[derive(Encode, Decode, Clone, TypeInfo)]
301#[scale_info(skip_type_params(T))]
302struct PasskeySignatureCheck<T: Config> {
303 payload: PasskeyPayloadV2<T>,
304 is_legacy_payload: bool,
305}
306
307impl<T: Config> PasskeySignatureCheck<T> {
308 pub fn new(passkey_payload: PasskeyPayloadV2<T>, is_legacy_payload: bool) -> Self {
309 Self { payload: passkey_payload, is_legacy_payload }
310 }
311
312 pub fn validate(&self) -> TransactionValidity {
313 let signed_data = self.payload.passkey_public_key.clone();
315 let signature = self.payload.account_ownership_proof.clone();
316 let signer = &self.payload.passkey_call.account_id;
317
318 Self::check_account_signature(signer, &signed_data, &signature)
319 .map_err(|_e| TransactionValidityError::Invalid(InvalidTransaction::BadSigner))?;
320
321 let p256_signed_data = match self.is_legacy_payload {
323 true => PasskeyPayload::from(self.payload.clone()).passkey_call.encode(),
324 false => self.payload.passkey_call.encode(),
325 };
326 let p256_signature = self.payload.verifiable_passkey_signature.clone();
327 let p256_signer = self.payload.passkey_public_key.clone();
328
329 p256_signature
330 .try_verify(&p256_signed_data, &p256_signer)
331 .map_err(|e| match e {
332 PasskeyVerificationError::InvalidProof =>
333 TransactionValidityError::Invalid(InvalidTransaction::BadSigner),
334 _ => TransactionValidityError::Invalid(InvalidTransaction::Custom(e.into())),
335 })?;
336
337 Ok(ValidTransaction::default())
338 }
339
340 pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
341 let _ = self.validate()?;
342 Ok(())
343 }
344
345 fn check_account_signature<P>(
356 signer: &T::AccountId,
357 payload: &P,
358 signature: &MultiSignature,
359 ) -> DispatchResult
360 where
361 P: Encode + EIP712Encode,
362 {
363 let key = T::ConvertIntoAccountId32::convert((*signer).clone());
364
365 if !check_signature(signature, key, payload) {
366 return Err(Error::<T>::InvalidAccountSignature.into());
367 }
368
369 Ok(())
370 }
371}
372
373#[derive(Encode, Decode, Clone, TypeInfo)]
375#[scale_info(skip_type_params(T))]
376pub struct ChargeTransactionPayment<T: Config>(pub T::AccountId, pub Call<T>);
377
378impl<T: Config> ChargeTransactionPayment<T>
379where
380 BalanceOf<T>: Send + Sync + From<u64>,
381 <T as frame_system::Config>::RuntimeCall:
382 From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
383 <T as frame_system::Config>::RuntimeOrigin: AsTransactionAuthorizedOrigin,
384{
385 pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
387 let info = &self.1.get_dispatch_info();
388 let len = self.1.using_encoded(|c| c.len());
389 let runtime_call: <T as frame_system::Config>::RuntimeCall =
390 <T as frame_system::Config>::RuntimeCall::from(self.1.clone());
391
392 let raw_origin = RawOrigin::from(Some(self.0.clone()));
393 let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
394 pallet_transaction_payment::ChargeTransactionPayment::<T>::from(Zero::zero())
395 .validate_and_prepare(who, &runtime_call, info, len, 4)?;
396 Ok(())
397 }
398
399 pub fn validate(&self) -> TransactionValidity {
401 let info = &self.1.get_dispatch_info();
402 let len = self.1.using_encoded(|c| c.len());
403 let runtime_call: <T as frame_system::Config>::RuntimeCall =
404 <T as frame_system::Config>::RuntimeCall::from(self.1.clone());
405 let raw_origin = RawOrigin::from(Some(self.0.clone()));
406 let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
407
408 let (res, _, _) =
409 pallet_transaction_payment::ChargeTransactionPayment::<T>::from(Zero::zero())
410 .validate(
411 who,
412 &runtime_call,
413 info,
414 len,
415 (),
416 &TxBaseImplication(runtime_call.clone()), TransactionSource::External,
418 )?;
419 Ok(res)
420 }
421}
422
423#[derive(Encode, Decode, Clone, TypeInfo)]
425#[scale_info(skip_type_params(T))]
426pub struct FrameSystemChecks<T: Config + Send + Sync>(pub T::AccountId, pub Call<T>);
427
428impl<T: Config + Send + Sync> FrameSystemChecks<T>
429where
430 <T as frame_system::Config>::RuntimeCall:
431 From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
432{
433 pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
435 let info = &self.1.get_dispatch_info();
436 let len = self.1.using_encoded(|c| c.len());
437 let runtime_call: <T as frame_system::Config>::RuntimeCall =
438 <T as frame_system::Config>::RuntimeCall::from(self.1.clone());
439
440 let raw_origin = RawOrigin::from(Some(self.0.clone()));
441 let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
442
443 let non_zero_sender_check = frame_system::CheckNonZeroSender::<T>::new();
444 let spec_version_check = frame_system::CheckSpecVersion::<T>::new();
445 let tx_version_check = frame_system::CheckTxVersion::<T>::new();
446 let genesis_hash_check = frame_system::CheckGenesis::<T>::new();
447 let era_check = frame_system::CheckEra::<T>::from(Era::immortal());
448
449 non_zero_sender_check.prepare((), &who, &runtime_call, info, len)?;
452 spec_version_check.prepare((), &who, &runtime_call, info, len)?;
453 tx_version_check.prepare((), &who, &runtime_call, info, len)?;
454 genesis_hash_check.prepare((), &who, &runtime_call, info, len)?;
455 era_check.prepare((), &who, &runtime_call, info, len)
456 }
457
458 pub fn validate(&self) -> TransactionValidity {
460 let info = &self.1.get_dispatch_info();
461 let len = self.1.using_encoded(|c| c.len());
462 let runtime_call: <T as frame_system::Config>::RuntimeCall =
463 <T as frame_system::Config>::RuntimeCall::from(self.1.clone());
464 let implication = TxBaseImplication(runtime_call.clone());
465 let raw_origin = RawOrigin::from(Some(self.0.clone()));
466 let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
467
468 let non_zero_sender_check = frame_system::CheckNonZeroSender::<T>::new();
469 let spec_version_check = frame_system::CheckSpecVersion::<T>::new();
470 let tx_version_check = frame_system::CheckTxVersion::<T>::new();
471 let genesis_hash_check = frame_system::CheckGenesis::<T>::new();
472 let era_check = frame_system::CheckEra::<T>::from(Era::immortal());
473
474 let (non_zero_sender_validity, _, origin) = non_zero_sender_check.validate(
475 who.clone(),
476 &runtime_call,
477 info,
478 len,
479 (),
480 &implication,
481 TransactionSource::External,
482 )?;
483
484 let (spec_version_validity, _, origin) = spec_version_check.validate(
485 origin,
486 &runtime_call,
487 info,
488 len,
489 spec_version_check.implicit()?,
490 &implication,
491 TransactionSource::External,
492 )?;
493
494 let (tx_version_validity, _, origin) = tx_version_check.validate(
495 origin,
496 &runtime_call,
497 info,
498 len,
499 tx_version_check.implicit()?,
500 &implication,
501 TransactionSource::External,
502 )?;
503
504 let (genesis_hash_validity, _, origin) = genesis_hash_check.validate(
505 origin,
506 &runtime_call,
507 info,
508 len,
509 genesis_hash_check.implicit()?,
510 &implication,
511 TransactionSource::External,
512 )?;
513
514 let (era_validity, _, _) = era_check.validate(
515 origin,
516 &runtime_call,
517 info,
518 len,
519 era_check.implicit()?,
520 &implication,
521 TransactionSource::External,
522 )?;
523
524 Ok(non_zero_sender_validity
525 .combine_with(spec_version_validity)
526 .combine_with(tx_version_validity)
527 .combine_with(genesis_hash_validity)
528 .combine_with(era_validity))
529 }
530}
531
532#[derive(Encode, Decode, Clone, TypeInfo)]
534#[scale_info(skip_type_params(T))]
535pub struct PasskeyWeightCheck<T: Config>(pub T::AccountId, pub Call<T>);
536
537impl<T: Config> PasskeyWeightCheck<T>
538where
539 <T as frame_system::Config>::RuntimeCall:
540 From<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
541{
542 pub fn new(account_id: T::AccountId, call: Call<T>) -> Self {
544 Self(account_id, call)
545 }
546
547 pub fn validate(&self) -> TransactionValidity {
549 let info = &self.1.get_dispatch_info();
550 let len = self.1.using_encoded(|c| c.len());
551 let runtime_call: <T as frame_system::Config>::RuntimeCall =
552 <T as frame_system::Config>::RuntimeCall::from(self.1.clone());
553 let implication = TxBaseImplication(runtime_call.clone());
554 let raw_origin = RawOrigin::from(Some(self.0.clone()));
555 let who = <T as frame_system::Config>::RuntimeOrigin::from(raw_origin);
556
557 let check_weight = CheckWeight::<T>::new();
558 let (result, _, _) = check_weight.validate(
559 who,
560 &runtime_call,
561 info,
562 len,
563 (),
564 &implication,
565 TransactionSource::External,
566 )?;
567 Ok(result)
568 }
569
570 pub fn pre_dispatch(&self) -> Result<(), TransactionValidityError> {
572 let info = &self.1.get_dispatch_info();
573 let len = self.1.using_encoded(|c| c.len());
574 let runtime_call: <T as frame_system::Config>::RuntimeCall =
575 <T as frame_system::Config>::RuntimeCall::from(self.1.clone());
576
577 CheckWeight::<T>::bare_validate_and_prepare(&runtime_call, info, len)
578 }
579}