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