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