diff --git a/README.md b/README.md index 8b082058984c748a53d2f7ba913d003697475f1a..38eb3a801b0c9dcd4f4974323548c194ff36c793 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,11 @@ Secret and/or public keys can always be passed using `--secret` and `--address`. Secret key format can be changed using `--secret-format` with the following values: * `substrate`: a Substrate secret address (optionally followed by a derivation path), or BIP39 mnemonic * `seed`: a 32-bytes seed in hexadecimal (Duniter v1 compatible) + +## TODO + +- [x] implement config formatter +- [x] add link/unlink account commands +- [ ] migrate all xt to submit_call_and_look_event +- [ ] add transfer with unit (ÄžD, UD...) +- [ ] \ No newline at end of file diff --git a/doc/config.md b/doc/config.md new file mode 100644 index 0000000000000000000000000000000000000000..541c0a0cc96e0fecea4c18011c779628942f9425 --- /dev/null +++ b/doc/config.md @@ -0,0 +1,27 @@ +# Äžcli config + +Some Äžcli commands require to have an address configured (for example to get account balance), some require to have a secret configured (to sign extrinsics). +Äžcli allows to save what you want in a config file and to overwrite parts in command line arguments. Example: + +```sh +# save Alice secret to config file +cargo run -- -S predefined -s Alice config save + +# show config +cargo run -- config show +# [stdout] +# Äžcli config +# duniter endpoint ws://localhost:9944 +# indexer endpoint http://localhost:8080/v1/graphql +# address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY (secret defined) + +# use different address in command line +cargo run -- --address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n config show +# [stdout] +# Äžcli config +# duniter endpoint ws://localhost:9944 +# indexer endpoint http://localhost:8080/v1/graphql +# address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n (no secret) +``` + +You can see that if a secret is defined, the associated address is used, but if an other address is given, the secret is silenced. \ No newline at end of file diff --git a/doc/example.md b/doc/example.md index 51cf095ef866f209daa2d00a3164e4d69678f545..7d3ea4515e4e8d76037cbca39f9763e5bbab7a54 100644 --- a/doc/example.md +++ b/doc/example.md @@ -20,7 +20,7 @@ gcli -S predefined -s Alice config save gcli --network local -S predefined -s test1 config save ``` -In the following, we assume this last command was run. +In the following, we assume this last command was run. More about the config in [config.md](./config.md). ## Commands diff --git a/src/commands/account.rs b/src/commands/account.rs index dc86d706acaceeaa13d5121f537e6e4c2485ca5b..3d8844d96a2421dd31ef5038d3c3b26840bcb951 100644 --- a/src/commands/account.rs +++ b/src/commands/account.rs @@ -27,6 +27,8 @@ pub enum Subcommand { /// List of target addresses dests: Vec<AccountId>, }, + /// Unlink the account from the linked identity + Unlink, } /// handle account commands @@ -48,6 +50,9 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( Subcommand::TransferMultiple { amount, dests } => { commands::transfer::transfer_multiple(&data, amount, dests).await?; } + Subcommand::Unlink => { + unlink_account(&data).await?; + } }; Ok(()) @@ -78,3 +83,12 @@ pub async fn get_account_info( .fetch(&runtime::storage().system().account(account_id), None) .await } + +/// unlink account +pub async fn unlink_account(data: &Data) -> Result<(), subxt::Error> { + submit_call_and_look_event::< + runtime::account::events::AccountUnlinked, + StaticTxPayload<runtime::account::calls::UnlinkIdentity>, + >(data, &runtime::tx().account().unlink_identity()) + .await +} diff --git a/src/commands/certification.rs b/src/commands/certification.rs index 69a2f11eb4f81d2e21471497d0e3ae47b2b3a17b..366b85321933feff3a61bc7c552476eb5611712d 100644 --- a/src/commands/certification.rs +++ b/src/commands/certification.rs @@ -2,31 +2,17 @@ use crate::*; /// submit a certification and track progress pub async fn certify(data: &Data, receiver: u32) -> Result<(), anyhow::Error> { - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx().cert().add_cert(data.idty_index(), receiver), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - + let progress = submit_call( + data, + &runtime::tx().cert().add_cert(data.idty_index(), receiver), + ) + .await?; if data.args.no_wait { return Ok(()); } let events = track_progress(progress).await?; - // look for the expected event - let new_cert_event = events.find_first::<runtime::cert::events::NewCert>()?; - let renew_cert_event = events.find_first::<runtime::cert::events::RenewedCert>()?; - - if let Some(event) = new_cert_event { - println!("{event:?}"); - } - if let Some(event) = renew_cert_event { - println!("{event:?}"); - } - + look_event::<runtime::cert::events::NewCert>(&events)?; + look_event::<runtime::cert::events::RenewedCert>(&events)?; Ok(()) } diff --git a/src/commands/collective.rs b/src/commands/collective.rs index 5ca295e7958ec04e772c54edf256bd6d8081e6a9..cc7f10e5819506004c769bc56bacf3c83c67aa86 100644 --- a/src/commands/collective.rs +++ b/src/commands/collective.rs @@ -135,26 +135,14 @@ pub async fn technical_committee_vote( proposal_index: u32, vote: bool, ) -> Result<(), subxt::Error> { - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx() - .technical_committee() - .vote(proposal_hash, proposal_index, vote), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; - - if let Some(e) = events.find_first::<runtime::technical_committee::events::Voted>()? { - println!("{e:?}"); - } - - Ok(()) + submit_call_and_look_event::< + runtime::technical_committee::events::Voted, + StaticTxPayload<runtime::technical_committee::calls::Vote>, + >( + data, + &runtime::tx() + .technical_committee() + .vote(proposal_hash, proposal_index, vote), + ) + .await } diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 02f75fbbd0e34379a218f89a6e2e266b8b30e89e..5543854cac331ce31bf47a86be8317b8e90faca0 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -3,7 +3,9 @@ use crate::*; use crate::{ commands::revocation::generate_revoc_doc, runtime::runtime_types::{ - common_runtime::entities::IdtyData, pallet_identity::types::*, sp_core::sr25519::Signature, + common_runtime::entities::{IdtyData, NewOwnerKeySignature}, + pallet_identity::types::*, + sp_core::sr25519::Signature, sp_runtime::MultiSignature, }, }; @@ -32,6 +34,12 @@ pub enum Subcommand { /// /// To be called by the certified not-yet-member account, to become member. Confirm { name: String }, + /// Validate an identity + /// Should be called when the distance has been evaluated positively + Validate { index: u32 }, + /// Renew membership + /// When membership comes to and end, it should be renewed for the identity to stay member + RenewMembership, /// Certify an identity Certify { target: u32 }, /// Revoke an identity immediately @@ -40,6 +48,11 @@ pub enum Subcommand { GenRevocDoc, /// Display member count MemberCount, + /// Link an account to the identity + LinkAccount { + /// address of the account that has to be linked + address: AccountId, + }, } /// handle identity commands @@ -61,6 +74,12 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( Subcommand::Confirm { name } => { confirm_identity(&data, name).await?; } + Subcommand::Validate { index } => { + validate_identity(&data, index).await?; + } + Subcommand::RenewMembership => { + renew_membership(&data).await?; + } Subcommand::Certify { target } => { data = data.fetch_idty_index().await?; // TODO fetch target username / key / index @@ -69,7 +88,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( } Subcommand::Revoke => { data = data.fetch_idty_index().await?; - revoke_identity(data).await?; + revoke_identity(&data).await?; } Subcommand::GenRevocDoc => { data = data.fetch_idty_index().await?; @@ -88,6 +107,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( .unwrap() ) } + Subcommand::LinkAccount { address } => { + data = data.fetch_idty_index().await?; // idty index required for payload + link_account(&data, address).await?; + } }; Ok(()) @@ -186,50 +209,42 @@ pub async fn get_identity_by_index( /// created identity pub async fn create_identity(data: &Data, target: AccountId) -> Result<(), subxt::Error> { - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx().identity().create_identity(target), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; - if let Some(e) = events.find_first::<runtime::identity::events::IdtyCreated>()? { - println!("{e:?}"); - } - Ok(()) + submit_call_and_look_event::< + runtime::identity::events::IdtyCreated, + StaticTxPayload<runtime::identity::calls::CreateIdentity>, + >(data, &runtime::tx().identity().create_identity(target)) + .await } /// confirm identity pub async fn confirm_identity(data: &Data, name: String) -> Result<(), subxt::Error> { - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx().identity().confirm_identity(name), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; + submit_call_and_look_event::< + runtime::identity::events::IdtyConfirmed, + StaticTxPayload<runtime::identity::calls::ConfirmIdentity>, + >(data, &runtime::tx().identity().confirm_identity(name)) + .await +} - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; - if let Some(e) = events.find_first::<runtime::identity::events::IdtyConfirmed>()? { - println!("{e:?}"); - } - Ok(()) +/// confirm identity +pub async fn validate_identity(data: &Data, index: u32) -> Result<(), subxt::Error> { + submit_call_and_look_event::< + runtime::identity::events::IdtyValidated, + StaticTxPayload<runtime::identity::calls::ValidateIdentity>, + >(data, &runtime::tx().identity().validate_identity(index)) + .await +} + +/// renew membership +pub async fn renew_membership(data: &Data) -> Result<(), subxt::Error> { + submit_call_and_look_event::< + runtime::membership::events::MembershipRenewed, + StaticTxPayload<runtime::membership::calls::RenewMembership>, + >(data, &runtime::tx().membership().renew_membership()) + .await } /// generate revokation document and submit it immediately -pub async fn revoke_identity(data: Data) -> Result<(), subxt::Error> { +pub async fn revoke_identity(data: &Data) -> Result<(), subxt::Error> { let (_payload, signature) = generate_revoc_doc(&data); // Transform signature to MultiSignature @@ -237,24 +252,45 @@ pub async fn revoke_identity(data: Data) -> Result<(), subxt::Error> { let signature = Signature(signature.0); let multisign = MultiSignature::Sr25519(signature); - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx() - .identity() - .revoke_identity(data.idty_index(), data.address(), multisign), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; + submit_call_and_look_event::< + runtime::identity::events::IdtyRemoved, + StaticTxPayload<runtime::identity::calls::RevokeIdentity>, + >( + data, + &runtime::tx() + .identity() + .revoke_identity(data.idty_index(), data.address(), multisign), + ) + .await +} - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; - if let Some(e) = events.find_first::<runtime::identity::events::IdtyRemoved>()? { - println!("{e:?}"); - } - Ok(()) +type LinkAccountPayload = Vec<u8>; +/// generate link account document +pub fn generate_link_account( + data: &Data, + address: AccountId, +) -> (LinkAccountPayload, sp_core::sr25519::Signature) { + let payload = (b"link", data.genesis_hash, data.idty_index(), address).encode(); + let signature = data.keypair().sign(&payload); + (payload, signature) +} + +/// link an account to the identity +pub async fn link_account(data: &Data, address: AccountId) -> Result<(), subxt::Error> { + let (_payload, signature) = generate_link_account(data, address.clone()); + + // this is a hack, see + // https://substrate.stackexchange.com/questions/10309/how-to-use-core-crypto-types-instead-of-runtime-types + let signature = Signature(signature.0); + + submit_call_and_look_event::< + runtime::account::events::AccountLinked, + StaticTxPayload<runtime::identity::calls::LinkAccount>, + >( + data, + &runtime::tx() + .identity() + .link_account(address, NewOwnerKeySignature(signature)), + ) + .await } diff --git a/src/commands/oneshot.rs b/src/commands/oneshot.rs index a395f78bd3a006f810f5e607e323ec2948cc1c58..c4cf3c362941e21b0aea0b462bdde9b03cd5e8dc 100644 --- a/src/commands/oneshot.rs +++ b/src/commands/oneshot.rs @@ -176,46 +176,31 @@ pub async fn consume_oneshot_account_with_remaining( .fetch(&runtime::storage().system().number(), None) .await? .unwrap(); - let progress = client - .tx() - .sign_and_submit_then_watch( - &runtime::tx() - .oneshot_account() - .consume_oneshot_account_with_remaining( - number, - if dest_oneshot { - runtime::runtime_types::pallet_oneshot_account::types::Account::Oneshot( - dest.into(), - ) - } else { - runtime::runtime_types::pallet_oneshot_account::types::Account::Normal( - dest.into(), - ) - }, - if remaining_to_oneshot { - runtime::runtime_types::pallet_oneshot_account::types::Account::Oneshot( - remaining_to.into(), - ) - } else { - runtime::runtime_types::pallet_oneshot_account::types::Account::Normal( - remaining_to.into(), - ) - }, - balance, - ), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; - if let Some(e) = - events.find_first::<runtime::oneshot_account::events::OneshotAccountConsumed>()? - { - println!("{e:?}"); - } - Ok(()) + let call = &runtime::tx() + .oneshot_account() + .consume_oneshot_account_with_remaining( + number, + if dest_oneshot { + runtime::runtime_types::pallet_oneshot_account::types::Account::Oneshot(dest.into()) + } else { + runtime::runtime_types::pallet_oneshot_account::types::Account::Normal(dest.into()) + }, + if remaining_to_oneshot { + runtime::runtime_types::pallet_oneshot_account::types::Account::Oneshot( + remaining_to.into(), + ) + } else { + runtime::runtime_types::pallet_oneshot_account::types::Account::Normal( + remaining_to.into(), + ) + }, + balance, + ); + + submit_call_and_look_event::< + runtime::oneshot_account::events::OneshotAccountConsumed, + StaticTxPayload<runtime::oneshot_account::calls::ConsumeOneshotAccountWithRemaining>, + >(data, call) + .await } diff --git a/src/commands/smith.rs b/src/commands/smith.rs index 8a69b7a727f52228d6749889473d12372091e85e..f7b25e21c12413cf85d7a30a7d4c783351a71f28 100644 --- a/src/commands/smith.rs +++ b/src/commands/smith.rs @@ -310,35 +310,21 @@ pub async fn online(data: &Data) -> Result<(), anyhow::Error> { Ok(()) } -/// submit a certification and track progress +/// submit a smith certification and track progress pub async fn cert(data: &Data, receiver: u32) -> Result<(), anyhow::Error> { - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx() - .smith_cert() - .add_cert(data.idty_index(), receiver), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - + let progress = submit_call( + data, + &runtime::tx() + .smith_cert() + .add_cert(data.idty_index(), receiver), + ) + .await?; if data.args.no_wait { return Ok(()); } let events = track_progress(progress).await?; - // look for the expected event - let new_cert_event = events.find_first::<runtime::smith_cert::events::NewCert>()?; - let renew_cert_event = events.find_first::<runtime::smith_cert::events::RenewedCert>()?; - - if let Some(event) = new_cert_event { - println!("{event:?}"); - } - if let Some(event) = renew_cert_event { - println!("{event:?}"); - } - + look_event::<runtime::smith_cert::events::NewCert>(&events)?; + look_event::<runtime::smith_cert::events::RenewedCert>(&events)?; Ok(()) } diff --git a/src/commands/sudo.rs b/src/commands/sudo.rs index 9e5d56cf5ea18e3a38d641889ff4b372ce058116..7b7b5053687eb01b839ce08b5535a2ad0969e028 100644 --- a/src/commands/sudo.rs +++ b/src/commands/sudo.rs @@ -2,19 +2,9 @@ use crate::*; /// set sudo key pub async fn set_key(data: &Data, new_key: AccountId) -> Result<(), subxt::Error> { - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx().sudo().set_key(new_key.into()), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - - if data.args.no_wait { - return Ok(()); - } - let _ = track_progress(progress).await?; // TODO - Ok(()) + submit_call_and_look_event::< + runtime::sudo::events::KeyChanged, + StaticTxPayload<runtime::sudo::calls::SetKey>, + >(data, &runtime::tx().sudo().set_key(new_key.into())) + .await } diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index c49512b89dc80b8acab25c1fa10a649fb7e57b87..555c05e7b428b76f75a848bbd153fe7887023feb 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -78,25 +78,10 @@ pub async fn transfer_multiple( }) }) .collect(); - // wrap these calls in a batch call - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx().utility().batch(transactions), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; - // TODO all transfer - if let Some(e) = events.find_first::<runtime::balances::events::Transfer>()? { - println!("{e:?}"); - } - Ok(()) + submit_call_and_look_event::< + runtime::utility::events::BatchCompleted, + StaticTxPayload<runtime::utility::calls::Batch>, + >(data, &runtime::tx().utility().batch(transactions)) + .await } diff --git a/src/commands/ud.rs b/src/commands/ud.rs index bda66f03909c87cde64ae697a7da193ff74432c4..28d7ef4e13826a9d2c1f61a3479b2c0ee9ad8438 100644 --- a/src/commands/ud.rs +++ b/src/commands/ud.rs @@ -15,7 +15,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( // match subcommand match command { Subcommand::Claim => { - claim_ud(data).await?; + claim_ud(&data).await?; } }; @@ -23,24 +23,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( } /// claim universal dividend -pub async fn claim_ud(data: Data) -> Result<(), anyhow::Error> { - let progress = data - .client() - .tx() - .sign_and_submit_then_watch( - &runtime::tx().universal_dividend().claim_uds(), - &PairSigner::new(data.keypair()), - BaseExtrinsicParamsBuilder::new(), - ) - .await?; - - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; - - if let Some(e) = events.find_first::<runtime::universal_dividend::events::UdsClaimed>()? { - println!("{e:?}"); - } - Ok(()) +pub async fn claim_ud(data: &Data) -> Result<(), subxt::Error> { + submit_call_and_look_event::< + runtime::universal_dividend::events::UdsClaimed, + StaticTxPayload<runtime::universal_dividend::calls::ClaimUds>, + >(data, &runtime::tx().universal_dividend().claim_uds()) + .await } diff --git a/src/conf.rs b/src/conf.rs index 5b7976bdca743ac066a777139845350e307ef223..9801172134129417509ae5b240e2f87421f87da4 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -27,6 +27,25 @@ impl std::default::Default for Config { } } +impl std::fmt::Display for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let secret = if self.secret.is_some() { + "(secret defined)" + } else { + "(no secret)" + }; + let address = if let Some(address) = &self.address { + format!("{}", address) + } else { + "(no address)".to_string() + }; + writeln!(f, "Äžcli config")?; + writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?; + writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?; + write!(f, "address {address} {secret}") + } +} + /// load config file and manage error if could not pub fn load_conf() -> Config { match confy::load(APP_NAME, None) { @@ -68,7 +87,7 @@ pub fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> { ); } Subcommand::Show => { - println!("{:?}", data.cfg); + println!("{}", data.cfg); } Subcommand::Save => { confy::store(APP_NAME, None, &data.cfg).expect("unable to write config"); diff --git a/src/main.rs b/src/main.rs index e4c0b008508f7c00651d44bf841772ccc9bd8b9b..1c90aacc3cbf9037f90b0a032c2d2d89ffe1f39f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,7 +92,7 @@ pub enum Subcommand { Config(conf::Subcommand), } -/// maint function +/// main function #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), GcliError> { // init logger diff --git a/src/runtime_config.rs b/src/runtime_config.rs index d62c5e1172f96d69ab3eed69296462a8a4d0ff31..509c78551328ac90d72f713fe39f868bbfeb096b 100644 --- a/src/runtime_config.rs +++ b/src/runtime_config.rs @@ -1,3 +1,4 @@ +// #[allow(clippy::enum_variant_names)] #[cfg(feature = "gdev")] #[subxt::subxt( runtime_metadata_path = "res/metadata.scale", @@ -5,8 +6,8 @@ )] pub mod runtime { // IF NEEDED - // #[subxt(substitute_type = "spcore::sr25519::Signature")] - // use crate::gdev::runtime_types::sp_core::sr25519::Signature; + // #[subxt(substitute_type = "sp_core::sr25519::Signature")] + // use crate::runtime::runtime_types::sp_core::sr25519::Signature; } // declare custom types diff --git a/src/utils.rs b/src/utils.rs index 4bfc80149e1281623d44c5f0c255c2fbd09cd89f..d3077f78708fde761f038ac5b6bcb05613dcf235 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -28,24 +28,40 @@ pub async fn submit_call_and_look_event<E: std::fmt::Debug + StaticEvent, Call: data: &Data, call: &Call, ) -> Result<(), subxt::Error> { - let progress = data - .client() + // submit call + let progress = submit_call(data, call).await?; + // if no wait, return immediately + if data.args.no_wait { + return Ok(()); + } + // collect events + let events = track_progress(progress).await?; + // print given event if there + look_event::<E>(&events) +} + +/// submit call +pub async fn submit_call<Call: TxPayload>( + data: &Data, + call: &Call, +) -> Result<TxProgress, subxt::Error> { + data.client() .tx() .sign_and_submit_then_watch( call, &PairSigner::new(data.keypair()), BaseExtrinsicParamsBuilder::new(), ) - .await?; + .await +} - if data.args.no_wait { - return Ok(()); - } - let events = track_progress(progress).await?; +/// look event +pub fn look_event<E: std::fmt::Debug + StaticEvent>( + events: &ExtrinsicEvents<Runtime>, +) -> Result<(), subxt::Error> { if let Some(e) = events.find_first::<E>()? { println!("{e:?}"); } - Ok(()) }