ink_e2e/
xts.rs

1// Copyright (C) Use Ink (UK) Ltd.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{
16    log_info,
17    sr25519,
18    Keypair,
19};
20use ink_env::Environment;
21
22use crate::contract_results::{
23    ContractExecResultFor,
24    ContractInstantiateResultFor,
25};
26use core::marker::PhantomData;
27use funty::Fundamental;
28use ink_primitives::DepositLimit;
29use pallet_revive::{
30    evm::{
31        CallTrace,
32        TracerConfig,
33        H160,
34    },
35    CodeUploadResult,
36};
37use sp_core::H256;
38use sp_runtime::OpaqueExtrinsic;
39use subxt::{
40    backend::{
41        legacy::LegacyRpcMethods,
42        rpc::RpcClient,
43    },
44    blocks::ExtrinsicEvents,
45    config::{
46        DefaultExtrinsicParams,
47        DefaultExtrinsicParamsBuilder,
48        ExtrinsicParams,
49        Header,
50    },
51    ext::{
52        scale_encode,
53        subxt_core::tx::Transaction,
54    },
55    tx::{
56        Signer,
57        SubmittableExtrinsic,
58        TxStatus,
59    },
60    OnlineClient,
61};
62
63/// Copied from `sp_weight` to additionally implement `scale_encode::EncodeAsType`.
64#[derive(
65    Copy,
66    Clone,
67    Eq,
68    PartialEq,
69    Debug,
70    Default,
71    scale::Encode,
72    scale::Decode,
73    scale::MaxEncodedLen,
74    scale_encode::EncodeAsType,
75    serde::Serialize,
76    serde::Deserialize,
77)]
78#[encode_as_type(crate_path = "subxt::ext::scale_encode")]
79pub struct Weight {
80    #[codec(compact)]
81    /// The weight of computational time used based on some reference hardware.
82    ref_time: u64,
83    #[codec(compact)]
84    /// The weight of storage space used by proof of validity.
85    proof_size: u64,
86}
87
88impl From<sp_weights::Weight> for Weight {
89    fn from(weight: sp_weights::Weight) -> Self {
90        Self {
91            ref_time: weight.ref_time(),
92            proof_size: weight.proof_size(),
93        }
94    }
95}
96
97impl From<Weight> for sp_weights::Weight {
98    fn from(weight: Weight) -> Self {
99        sp_weights::Weight::from_parts(weight.ref_time, weight.proof_size)
100    }
101}
102
103/// A raw call to `pallet-revive`'s `instantiate_with_code`.
104#[derive(Debug, scale::Encode, scale::Decode, scale_encode::EncodeAsType)]
105#[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")]
106pub struct InstantiateWithCode<E: Environment> {
107    #[codec(compact)]
108    value: E::Balance,
109    gas_limit: Weight,
110    #[codec(compact)]
111    storage_deposit_limit: E::Balance,
112    code: Vec<u8>,
113    data: Vec<u8>,
114    salt: Option<[u8; 32]>,
115}
116
117/// A raw call to `pallet-revive`'s `call`.
118#[derive(Debug, scale::Decode, scale::Encode, scale_encode::EncodeAsType)]
119#[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")]
120pub struct Call<E: Environment> {
121    dest: H160,
122    #[codec(compact)]
123    value: E::Balance,
124    gas_limit: Weight,
125    #[codec(compact)]
126    storage_deposit_limit: E::Balance,
127    data: Vec<u8>,
128}
129
130/// A raw call to `pallet-revive`'s `map_account`.
131#[derive(Debug, scale::Decode, scale::Encode, scale_encode::EncodeAsType)]
132#[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")]
133pub struct MapAccount {}
134
135/// A raw call to `pallet-revive`'s `call`.
136#[derive(Debug, scale::Decode, scale::Encode, scale_encode::EncodeAsType)]
137#[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")]
138pub struct Transfer<E: Environment, C: subxt::Config> {
139    dest: subxt::utils::Static<C::Address>,
140    #[codec(compact)]
141    value: E::Balance,
142}
143
144/// A raw call to `pallet-revive`'s `remove_code`.
145#[derive(Debug, scale::Encode, scale::Decode, scale_encode::EncodeAsType)]
146#[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")]
147pub struct RemoveCode {
148    code_hash: H256,
149}
150
151/// A raw call to `pallet-revive`'s `upload_code`.
152#[derive(Debug, scale::Encode, scale::Decode, scale_encode::EncodeAsType)]
153#[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")]
154pub struct UploadCode<E: Environment> {
155    code: Vec<u8>,
156    #[codec(compact)]
157    storage_deposit_limit: E::Balance,
158}
159
160/// A struct that encodes RPC parameters required to instantiate a new smart contract.
161#[derive(scale::Encode)]
162// todo: #[derive(serde::Serialize, scale::Encode)]
163// todo: #[serde(rename_all = "camelCase")]
164struct RpcInstantiateRequest<C: subxt::Config, E: Environment> {
165    origin: C::AccountId,
166    value: E::Balance,
167    gas_limit: Option<Weight>,
168    storage_deposit_limit: Option<E::Balance>,
169    code: Code,
170    data: Vec<u8>,
171    salt: Option<[u8; 32]>,
172}
173
174/// A struct that encodes RPC parameters required to upload a new smart contract.
175#[derive(scale::Encode)]
176// todo: #[derive(serde::Serialize, scale::Encode)]
177// todo: #[serde(rename_all = "camelCase")]
178struct RpcCodeUploadRequest<C: subxt::Config, E: Environment>
179where
180    E::Balance: serde::Serialize,
181{
182    origin: C::AccountId,
183    code: Vec<u8>,
184    storage_deposit_limit: Option<E::Balance>,
185}
186
187/// A struct that encodes RPC parameters required for a call to a smart contract.
188#[derive(scale::Encode)]
189// todo: #[derive(serde::Serialize, scale::Encode)]
190// todo: #[serde(rename_all = "camelCase")]
191struct RpcCallRequest<C: subxt::Config, E: Environment> {
192    origin: C::AccountId,
193    dest: H160,
194    value: E::Balance,
195    gas_limit: Option<Weight>,
196    storage_deposit_limit: Option<E::Balance>,
197    input_data: Vec<u8>,
198}
199
200/// Reference to an existing code hash or a new contract binary.
201#[derive(serde::Serialize, scale::Encode)]
202#[serde(rename_all = "camelCase")]
203enum Code {
204    /// A contract binary as raw bytes.
205    Upload(Vec<u8>),
206    #[allow(unused)]
207    /// The code hash of an on-chain contract blob.
208    Existing(H256),
209}
210
211/// Provides functions for interacting with the `pallet-revive` API.
212pub struct ReviveApi<C: subxt::Config, E: Environment> {
213    pub rpc: LegacyRpcMethods<C>,
214    pub client: OnlineClient<C>,
215    _phantom: PhantomData<fn() -> (C, E)>,
216}
217
218impl<C, E> ReviveApi<C, E>
219where
220    C: subxt::Config,
221    C::AccountId: From<sr25519::PublicKey> + serde::de::DeserializeOwned + scale::Codec,
222    C::Address: From<sr25519::PublicKey>,
223    C::Signature: From<sr25519::Signature>,
224    <C::ExtrinsicParams as ExtrinsicParams<C>>::Params:
225        From<<DefaultExtrinsicParams<C> as ExtrinsicParams<C>>::Params>,
226
227    E: Environment,
228    E::Balance: scale::HasCompact + serde::Serialize,
229{
230    /// Creates a new [`ReviveApi`] instance.
231    pub async fn new(rpc: RpcClient) -> Result<Self, subxt::Error> {
232        let client = OnlineClient::<C>::from_rpc_client(rpc.clone()).await?;
233        let rpc = LegacyRpcMethods::<C>::new(rpc);
234        Ok(Self {
235            rpc,
236            client,
237            _phantom: Default::default(),
238        })
239    }
240
241    /// Attempt to transfer the `value` from `origin` to `dest`.
242    ///
243    /// Returns `Ok` on success, and a [`subxt::Error`] if the extrinsic is
244    /// invalid (e.g. out of date nonce)
245    pub async fn try_transfer_balance(
246        &self,
247        origin: &Keypair,
248        dest: C::AccountId,
249        value: E::Balance,
250    ) -> Result<(), subxt::Error> {
251        let call = subxt::tx::DefaultPayload::new(
252            "Balances",
253            "transfer_allow_death",
254            Transfer::<E, C> {
255                dest: subxt::utils::Static(dest.into()),
256                value,
257            },
258        )
259        .unvalidated();
260
261        let _ = self.submit_extrinsic(&call, origin).await;
262
263        Ok(())
264    }
265
266    /// Dry runs the instantiation of the given `code`.
267    pub async fn instantiate_with_code_dry_run(
268        &self,
269        value: E::Balance,
270        storage_deposit_limit: DepositLimit<E::Balance>,
271        code: Vec<u8>,
272        data: Vec<u8>,
273        salt: Option<[u8; 32]>,
274        signer: &Keypair,
275    ) -> ContractInstantiateResultFor<E> {
276        let code = Code::Upload(code);
277        let storage_deposit_limit = match storage_deposit_limit {
278            DepositLimit::Balance(v) => Some(v),
279            DepositLimit::Unchecked => None,
280        };
281        let call_request = RpcInstantiateRequest::<C, E> {
282            origin: Signer::<C>::account_id(signer),
283            value,
284            gas_limit: None,
285            storage_deposit_limit,
286            code,
287            data,
288            salt,
289        };
290        let func = "ReviveApi_instantiate";
291        let params = scale::Encode::encode(&call_request);
292        let bytes = self
293            .rpc
294            .state_call(func, Some(&params), None)
295            .await
296            .unwrap_or_else(|err| {
297                panic!("error on ws request `revive_instantiate`: {err:?}");
298            });
299        scale::Decode::decode(&mut bytes.as_ref()).unwrap_or_else(|err| {
300            panic!("decoding `ContractInstantiateResult` failed: {err}")
301        })
302    }
303
304    /// todo
305    pub async fn create_extrinsic<Call>(
306        &self,
307        call: &Call,
308        signer: &Keypair,
309    ) -> SubmittableExtrinsic<C, OnlineClient<C>>
310    where
311        Call: subxt::tx::Payload,
312    {
313        let account_id = <Keypair as Signer<C>>::account_id(signer);
314        let account_nonce =
315            self.get_account_nonce(&account_id)
316                .await
317                .unwrap_or_else(|err| {
318                    panic!("error calling `get_account_nonce`: {err:?}");
319                });
320
321        let params = DefaultExtrinsicParamsBuilder::new()
322            .nonce(account_nonce)
323            .build();
324        self.client
325            .tx()
326            .create_signed_offline(call, signer, params.into())
327            .unwrap_or_else(|err| {
328                panic!("error on call `create_signed_with_nonce`: {err:?}");
329            })
330    }
331
332    /// Sign and submit an extrinsic with the given call payload.
333    pub async fn submit_extrinsic<Call>(
334        &self,
335        call: &Call,
336        signer: &Keypair,
337    ) -> (ExtrinsicEvents<C>, Option<CallTrace>)
338    where
339        Call: subxt::tx::Payload,
340    {
341        // we have to retrieve the current block hash here. we use it later in this
342        // function when retrieving the log. the extrinsic is dry-run for tracing
343        // the log. if we were to use the latest block the extrinsic would already
344        // have been executed and we would get an error.
345        let parent_hash = self.best_block().await;
346
347        let mut tx = self
348            .create_extrinsic(call, signer)
349            .await
350            .submit_and_watch()
351            .await
352            .inspect(|tx_progress| {
353                log_info(&format!(
354                    "signed and submitted tx with hash {:?}",
355                    tx_progress.extrinsic_hash()
356                ));
357            })
358            .unwrap_or_else(|err| {
359                panic!("error on call `submit_and_watch`: {err:?}");
360            });
361
362        // Below we use the low level API to replicate the `wait_for_in_block` behaviour
363        // which was removed in subxt 0.33.0. See https://github.com/paritytech/subxt/pull/1237.
364        //
365        // We require this because we use `ink-node` as our development
366        // node, which does not currently support finality, so we just want to
367        // wait until it is included in a block.
368        while let Some(status) = tx.next().await {
369            match status.unwrap_or_else(|err| {
370                panic!("error subscribing to tx status: {err:?}");
371            }) {
372                TxStatus::InBestBlock(tx_in_block)
373                | TxStatus::InFinalizedBlock(tx_in_block) => {
374                    let events = tx_in_block.fetch_events().await.unwrap_or_else(|err| {
375                        panic!("error on call `fetch_events`: {err:?}");
376                    });
377                    let trace = self
378                        .trace(
379                            tx_in_block.block_hash(),
380                            Some(tx_in_block.extrinsic_hash()),
381                            parent_hash,
382                            None,
383                        )
384                        .await;
385                    return (events, trace)
386                }
387                TxStatus::Error { message } => {
388                    panic!("TxStatus::Error: {message:?}");
389                }
390                TxStatus::Invalid { message } => {
391                    panic!("TxStatus::Invalid: {message:?}");
392                }
393                TxStatus::Dropped { message } => {
394                    panic!("TxStatus::Dropped: {message:?}");
395                }
396                _ => continue,
397            }
398        }
399        panic!("Error waiting for tx status")
400    }
401
402    /// todo
403    pub async fn trace(
404        &self,
405        block_hash: C::Hash,
406        extrinsic_hash: Option<C::Hash>,
407        parent_hash: C::Hash,
408        extrinsic: Option<Vec<u8>>,
409    ) -> Option<CallTrace> {
410        // todo move below to its own function
411        let block_details = self
412            .rpc
413            .chain_get_block(Some(block_hash))
414            .await
415            .expect("no block found")
416            .expect("no block details found");
417        let header = block_details.block.header;
418        let mut exts: Vec<OpaqueExtrinsic> = block_details
419            .block
420            .extrinsics
421            .clone()
422            .into_iter()
423            .filter_map(|e| scale::Decode::decode(&mut &e[..]).ok())
424            .collect::<Vec<_>>();
425
426        // todo
427        let tx_index: usize = match (extrinsic_hash, extrinsic) {
428            (Some(hash), None) => {
429                let index = block_details
430                    .block
431                    .extrinsics
432                    .iter()
433                    .cloned()
434                    .enumerate()
435                    .find_map(|(index, ext)| {
436                        let hash_ext = Transaction::<C>::from_bytes(ext.0).hash();
437                        if hash_ext == hash {
438                            return Some(index);
439                        }
440                        None
441                    })
442                    .expect("the extrinsic hash was not found in the block");
443                index
444            }
445            (None, Some(extrinsic)) => {
446                exts.push(
447                    OpaqueExtrinsic::from_bytes(&extrinsic[..])
448                        .expect("OpaqueExtrinsic cannot be created"),
449                );
450                exts.len() - 1
451            }
452            _ => panic!("pattern error"),
453        };
454
455        let tracer_config = TracerConfig::CallTracer { with_logs: true };
456        let func = "ReviveApi_trace_tx";
457
458        let params =
459            scale::Encode::encode(&((header, exts), tx_index.as_u32(), tracer_config));
460
461        let bytes = self
462            .rpc
463            .state_call(func, Some(&params), Some(parent_hash))
464            .await
465            .unwrap_or_else(|err| {
466                panic!(
467                    "error on ws request `trace_tx`: {err:?}\n\n{:#}",
468                    format!("{}", err).trim_start_matches("RPC error: ")
469                );
470            });
471        scale::Decode::decode(&mut bytes.as_ref())
472            .unwrap_or_else(|err| panic!("decoding `trace_tx` result failed: {err}"))
473    }
474
475    /// Return the hash of the *best* block
476    pub async fn best_block(&self) -> C::Hash {
477        self.rpc
478            .chain_get_block_hash(None)
479            .await
480            .unwrap_or_else(|err| {
481                panic!("error on call `chain_get_block_hash`: {err:?}");
482            })
483            .unwrap_or_else(|| {
484                panic!("error on call `chain_get_block_hash`: no best block found");
485            })
486    }
487
488    /// Return the account nonce at the *best* block for an account ID.
489    async fn get_account_nonce(
490        &self,
491        account_id: &C::AccountId,
492    ) -> Result<u64, subxt::Error> {
493        let best_block = self.best_block().await;
494        let account_nonce = self
495            .client
496            .blocks()
497            .at(best_block)
498            .await?
499            .account_nonce(account_id)
500            .await?;
501        Ok(account_nonce)
502    }
503
504    /// Submits an extrinsic to instantiate a contract with the given code.
505    ///
506    /// Returns when the transaction is included in a block. The return value
507    /// contains all events that are associated with this transaction.
508    #[allow(clippy::too_many_arguments)]
509    pub async fn instantiate_with_code(
510        &self,
511        value: E::Balance,
512        gas_limit: Weight,
513        storage_deposit_limit: E::Balance,
514        code: Vec<u8>,
515        data: Vec<u8>,
516        salt: Option<[u8; 32]>,
517        signer: &Keypair,
518    ) -> (ExtrinsicEvents<C>, Option<CallTrace>) {
519        let call = subxt::tx::DefaultPayload::new(
520            "Revive",
521            "instantiate_with_code",
522            InstantiateWithCode::<E> {
523                value,
524                gas_limit,
525                storage_deposit_limit, // todo
526                code,
527                data,
528                salt,
529            },
530        )
531        .unvalidated();
532
533        self.submit_extrinsic(&call, signer).await
534    }
535
536    /// Dry runs the upload of the given `code`.
537    pub async fn upload_dry_run(
538        &self,
539        signer: &Keypair,
540        code: Vec<u8>,
541        // todo
542        _storage_deposit_limit: E::Balance,
543    ) -> CodeUploadResult<E::Balance> {
544        let call_request = RpcCodeUploadRequest::<C, E> {
545            origin: Signer::<C>::account_id(signer),
546            code,
547            //storage_deposit_limit,
548            storage_deposit_limit: None,
549        };
550        let func = "ReviveApi_upload_code";
551        let params = scale::Encode::encode(&call_request);
552        let bytes = self
553            .rpc
554            .state_call(func, Some(&params), None)
555            .await
556            .unwrap_or_else(|err| {
557                panic!("error on ws request `upload_code`: {err:?}");
558            });
559        scale::Decode::decode(&mut bytes.as_ref())
560            .unwrap_or_else(|err| panic!("decoding CodeUploadResult failed: {err}"))
561    }
562
563    /// Submits an extrinsic to upload a given code.
564    ///
565    /// Returns when the transaction is included in a block. The return value
566    /// contains all events that are associated with this transaction.
567    pub async fn upload(
568        &self,
569        signer: &Keypair,
570        code: Vec<u8>,
571        storage_deposit_limit: E::Balance,
572    ) -> ExtrinsicEvents<C> {
573        let call = subxt::tx::DefaultPayload::new(
574            "Revive",
575            "upload_code",
576            UploadCode::<E> {
577                code,
578                storage_deposit_limit,
579                //storage_deposit_limit: None
580            },
581        )
582        .unvalidated();
583
584        self.submit_extrinsic(&call, signer).await.0
585    }
586
587    /// Submits an extrinsic to remove the code at the given hash.
588    ///
589    /// Returns when the transaction is included in a block. The return value
590    /// contains all events that are associated with this transaction.
591    pub async fn remove_code(
592        &self,
593        signer: &Keypair,
594        code_hash: H256,
595    ) -> ExtrinsicEvents<C> {
596        let call = subxt::tx::DefaultPayload::new(
597            "Revive",
598            "remove_code",
599            RemoveCode { code_hash },
600        )
601        .unvalidated();
602
603        self.submit_extrinsic(&call, signer).await.0
604    }
605
606    /// Dry runs a call of the contract at `contract` with the given parameters.
607    pub async fn call_dry_run(
608        &self,
609        origin: C::AccountId,
610        dest: H160,
611        input_data: Vec<u8>,
612        value: E::Balance,
613        _storage_deposit_limit: E::Balance, // todo
614        signer: &Keypair,
615    ) -> (ContractExecResultFor<E>, Option<CallTrace>) {
616        let call_request = RpcCallRequest::<C, E> {
617            origin,
618            dest,
619            value,
620            gas_limit: Some(Weight {
621                ref_time: u64::MAX,
622                proof_size: u64::MAX,
623            }),
624            storage_deposit_limit: None,
625            input_data: input_data.clone(),
626        };
627        let func = "ReviveApi_call";
628        let params = scale::Encode::encode(&call_request);
629        let bytes = self
630            .rpc
631            .state_call(func, Some(&params), None)
632            .await
633            .unwrap_or_else(|err| {
634                panic!("error on ws request `contracts_call`: {err:?}");
635            });
636        let res: ContractExecResultFor<E> = scale::Decode::decode(&mut bytes.as_ref())
637            .unwrap_or_else(|err| panic!("decoding ContractExecResult failed: {err}"));
638
639        // todo for gas_limit and storage_deposit_limit we should use the values returned
640        // by a successful call above, otherwise the max.
641
642        // and now collect the trace and put it in there as well.
643        let call = subxt::tx::DefaultPayload::new(
644            "Revive",
645            "call",
646            crate::xts::Call::<E> {
647                dest,
648                value,
649                gas_limit: Weight {
650                    ref_time: u64::MAX,
651                    proof_size: u64::MAX,
652                },
653                storage_deposit_limit: E::Balance::from(u32::MAX), // todo
654                data: input_data,
655            },
656        )
657        .unvalidated();
658        let xt = self.create_extrinsic(&call, signer).await;
659
660        let block_hash = self.best_block().await;
661
662        let block_details = self
663            .rpc
664            .chain_get_block(Some(block_hash))
665            .await
666            .expect("no block found")
667            .expect("no block details found");
668        // let header = block_details.block.header;
669        let block_number: u64 = block_details.block.header.number().into();
670        let parent_hash = self
671            .rpc
672            .chain_get_block_hash(Some((block_number - 1u64).into()))
673            .await
674            .expect("no block hash found")
675            .expect("no block details found");
676
677        let trace = self
678            .trace(block_hash, None, parent_hash, Some(xt.into_encoded()))
679            .await;
680
681        (res, trace)
682    }
683
684    /// Submits an extrinsic to call a contract with the given parameters.
685    ///
686    /// Returns when the transaction is included in a block. The return value
687    /// contains all events that are associated with this transaction.
688    /// todo the API for `call_dry_run` should mirror that of `call`
689    pub async fn call(
690        &self,
691        contract: H160,
692        value: E::Balance,
693        gas_limit: Weight,
694        storage_deposit_limit: E::Balance,
695        data: Vec<u8>,
696        signer: &Keypair,
697    ) -> (ExtrinsicEvents<C>, Option<CallTrace>) {
698        let call = subxt::tx::DefaultPayload::new(
699            "Revive",
700            "call",
701            Call::<E> {
702                dest: contract,
703                value,
704                gas_limit,
705                storage_deposit_limit,
706                data,
707            },
708        )
709        .unvalidated();
710
711        self.submit_extrinsic(&call, signer).await
712    }
713
714    /// todo
715    /// Submits an extrinsic to call a contract with the given parameters.
716    ///
717    /// Returns when the transaction is included in a block. The return value
718    /// contains all events that are associated with this transaction.
719    pub async fn map_account(&self, signer: &Keypair) -> ExtrinsicEvents<C> {
720        let call = subxt::tx::DefaultPayload::new("Revive", "map_account", MapAccount {})
721            .unvalidated();
722
723        self.submit_extrinsic(&call, signer).await.0
724    }
725
726    /// Submit an extrinsic `call_name` for the `pallet_name`.
727    /// The `call_data` is a `Vec<subxt::dynamic::Value>` that holds
728    /// a representation of some value.
729    ///
730    /// Returns when the transaction is included in a block. The return value
731    /// contains all events that are associated with this transaction.
732    pub async fn runtime_call<'a>(
733        &self,
734        signer: &Keypair,
735        pallet_name: &'a str,
736        call_name: &'a str,
737        call_data: Vec<subxt::dynamic::Value>,
738    ) -> ExtrinsicEvents<C> {
739        let call = subxt::dynamic::tx(pallet_name, call_name, call_data);
740
741        self.submit_extrinsic(&call, signer).await.0
742    }
743}