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