diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0a7670598b..d5d8d62982 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -70,7 +70,10 @@ jobs: # Subxt-signer has the "subxt" features enabled in the "check all targets" test. Run it on its own to # check it without. We can't enable subxt or web features here, so no cargo hack. - name: Cargo check subxt-signer - run: cargo check -p subxt-signer + run: | + cargo check -p subxt-signer + cargo check -p subxt-signer --no-default-features --features sr25519 + cargo check -p subxt-signer --no-default-features --features ecdsa # We can't enable web features here, so no cargo hack. - name: Cargo check subxt-lightclient diff --git a/Cargo.lock b/Cargo.lock index 700794709d..ba18410806 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3314,7 +3314,16 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.6.1", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys 0.8.1", ] [[package]] @@ -3326,6 +3335,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -3727,7 +3745,7 @@ dependencies = [ "regex", "scale-info", "schnorrkel 0.9.1", - "secp256k1", + "secp256k1 0.24.3", "secrecy", "serde", "sp-core-hashing", @@ -3807,7 +3825,7 @@ dependencies = [ "log", "parity-scale-codec", "rustversion", - "secp256k1", + "secp256k1 0.24.3", "sp-core", "sp-externalities", "sp-keystore", @@ -4290,6 +4308,7 @@ dependencies = [ "pbkdf2 0.12.2", "regex", "schnorrkel 0.10.2", + "secp256k1 0.27.0", "secrecy", "sha2 0.10.7", "sp-core", diff --git a/Cargo.toml b/Cargo.toml index 43f5563273..f62f106c7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,6 +116,7 @@ bip39 = "2.0.0" hmac = "0.12.1" pbkdf2 = { version = "0.12.2", default-features = false } schnorrkel = "0.10.2" +secp256k1 = "0.27.0" secrecy = "0.8.0" sha2 = "0.10.6" -zeroize = { version = "1", default-features = false } \ No newline at end of file +zeroize = { version = "1", default-features = false } diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 8ee9e3bee2..3b4398c303 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -15,7 +15,14 @@ description = "Sign extrinsics to be submitted by Subxt" keywords = ["parity", "subxt", "extrinsic", "signer"] [features] -default = [] +default = ["sr25519", "ecdsa"] + +# Pick the signer implementation(s) you need by enabling the +# corresponding features. Note: I had more difficulties getting +# ecdsa compiling to WASM on my mac; following this comment helped: +# https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699 +sr25519 = ["schnorrkel"] +ecdsa = ["secp256k1"] # Make the keypair algorithms here compatible with Subxt's Signer trait, # so that they can be used to sign transactions for compatible chains. @@ -37,7 +44,8 @@ sha2 = { workspace = true } hmac = { workspace = true } zeroize = { workspace = true } bip39 = { workspace = true } -schnorrkel = { workspace = true } +schnorrkel = { workspace = true, optional = true } +secp256k1 = { workspace = true, features = ["recovery", "global-context"], optional = true } secrecy = { workspace = true } # We only pull this in to enable the JS flag for schnorrkel to use. diff --git a/signer/src/crypto/seed_from_entropy.rs b/signer/src/crypto/seed_from_entropy.rs index ba51080659..7643ff7835 100644 --- a/signer/src/crypto/seed_from_entropy.rs +++ b/signer/src/crypto/seed_from_entropy.rs @@ -10,6 +10,7 @@ use zeroize::Zeroize; /// This is taken from `substrate-bip39` so that we can keep dependencies in line, and /// is the same logic that sp-core uses to go from mnemonic entropy to seed. Returns /// `None` if invalid length. +#[allow(dead_code)] pub fn seed_from_entropy(entropy: &[u8], password: &str) -> Option<[u8; 64]> { if entropy.len() < 16 || entropy.len() > 32 || entropy.len() % 4 != 0 { return None; diff --git a/signer/src/ecdsa.rs b/signer/src/ecdsa.rs new file mode 100644 index 0000000000..e5b0900512 --- /dev/null +++ b/signer/src/ecdsa.rs @@ -0,0 +1,420 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! An ecdsa keypair implementation. +use codec::Encode; + +use crate::crypto::{seed_from_entropy, DeriveJunction, SecretUri}; +use hex::FromHex; +use secp256k1::{ecdsa::RecoverableSignature, Message, SecretKey, SECP256K1}; +use secrecy::ExposeSecret; + +const SEED_LENGTH: usize = 32; + +/// Seed bytes used to generate a key pair. +pub type Seed = [u8; SEED_LENGTH]; + +/// A signature generated by [`Keypair::sign()`]. These bytes are equivalent +/// to a Substrate `MultiSignature::Ecdsa(bytes)`. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Signature(pub [u8; 65]); + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// The (compressed) public key for an [`Keypair`] key pair. +#[derive(Debug, Clone)] +pub struct PublicKey(pub [u8; 33]); + +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// An ecdsa keypair implementation. +#[derive(Debug, Clone)] +pub struct Keypair(pub secp256k1::KeyPair); + +impl Keypair { + /// Create an ecdsa keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more. + /// + /// # Example + /// + /// ```rust + /// use subxt_signer::{ SecretUri, ecdsa::Keypair }; + /// use std::str::FromStr; + /// + /// let uri = SecretUri::from_str("//Alice").unwrap(); + /// let keypair = Keypair::from_uri(&uri).unwrap(); + /// + /// keypair.sign(b"Hello world!"); + /// ``` + pub fn from_uri(uri: &SecretUri) -> Result { + let SecretUri { + junctions, + phrase, + password, + } = uri; + + // If the phrase is hex, convert bytes directly into a seed, ignoring password. + // Else, parse the phrase string taking the password into account. This is + // the same approach taken in sp_core::crypto::Pair::from_string_with_seed. + let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") { + let seed = Seed::from_hex(hex_str)?; + Self::from_seed(seed)? + } else { + let phrase = bip39::Mnemonic::parse(phrase.expose_secret().as_str())?; + let pass_str = password.as_ref().map(|p| p.expose_secret().as_str()); + Self::from_phrase(&phrase, pass_str)? + }; + + // Now, use any "junctions" to derive a new key from this root key. + key.derive(junctions.iter().copied()) + } + + /// Create an ecdsa keypair from a BIP-39 mnemonic phrase and optional password. + /// + /// # Example + /// + /// ```rust + /// use subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair }; + /// + /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; + /// let mnemonic = Mnemonic::parse(phrase).unwrap(); + /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap(); + /// + /// keypair.sign(b"Hello world!"); + /// ``` + pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result { + let big_seed = seed_from_entropy(&mnemonic.to_entropy(), password.unwrap_or("")) + .ok_or(Error::InvalidSeed)?; + + let seed: Seed = big_seed[..SEED_LENGTH] + .try_into() + .expect("should be valid Seed"); + + Self::from_seed(seed) + } + + /// Turn a 32 byte seed into a keypair. + /// + /// # Warning + /// + /// This will only be secure if the seed is secure! + pub fn from_seed(seed: Seed) -> Result { + let secret = SecretKey::from_slice(&seed).map_err(|_| Error::InvalidSeed)?; + Ok(Self(secp256k1::KeyPair::from_secret_key( + SECP256K1, &secret, + ))) + } + + /// Derive a child key from this one given a series of junctions. + /// + /// # Example + /// + /// ```rust + /// use subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair, DeriveJunction }; + /// + /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; + /// let mnemonic = Mnemonic::parse(phrase).unwrap(); + /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap(); + /// + /// // Equivalent to the URI path '//Alice//stash': + /// let new_keypair = keypair.derive([ + /// DeriveJunction::hard("Alice"), + /// DeriveJunction::hard("stash") + /// ]); + /// ``` + pub fn derive>( + &self, + junctions: Js, + ) -> Result { + let mut acc = self.0.secret_key().clone().secret_bytes(); + for junction in junctions { + match junction { + DeriveJunction::Soft(_) => return Err(Error::SoftJunction), + DeriveJunction::Hard(junction_bytes) => { + acc = ("Secp256k1HDKD", acc, junction_bytes) + .using_encoded(sp_core_hashing::blake2_256) + } + } + } + Self::from_seed(acc) + } + + /// Obtain the [`PublicKey`] part of this key pair, which can be used in calls to [`verify()`]. + /// or otherwise converted into an address. In case of ECDSA, the public key bytes are not + /// equivalent to a Substrate `AccountId32`. They have to be hashed to obtain `AccountId32`. + pub fn public_key(&self) -> PublicKey { + PublicKey(self.0.public_key().serialize()) + } + + /// Sign some message. These bytes can be used directly in a Substrate `MultiSignature::Ecdsa(..)`. + pub fn sign(&self, message: &[u8]) -> Signature { + // From sp_core::ecdsa::sign: + let message_hash = sp_core_hashing::blake2_256(message); + // From sp_core::ecdsa::sign_prehashed: + let wrapped = Message::from_slice(&message_hash).expect("Message is 32 bytes; qed"); + let recsig: RecoverableSignature = + SECP256K1.sign_ecdsa_recoverable(&wrapped, &self.0.secret_key()); + // From sp_core::ecdsa's `impl From for Signature`: + let (recid, sig) = recsig.serialize_compact(); + let mut signature_bytes: [u8; 65] = [0; 65]; + signature_bytes[..64].copy_from_slice(&sig); + signature_bytes[64] = (recid.to_i32() & 0xFF) as u8; + Signature(signature_bytes) + } +} + +/// Verify that some signature for a message was created by the owner of the [`PublicKey`]. +/// +/// ```rust +/// use subxt_signer::{ bip39::Mnemonic, ecdsa }; +/// +/// let keypair = ecdsa::dev::alice(); +/// let message = b"Hello!"; +/// +/// let signature = keypair.sign(message); +/// let public_key = keypair.public_key(); +/// assert!(ecdsa::verify(&signature, message, &public_key)); +/// ``` +pub fn verify>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool { + let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.0[..64]) else { + return false; + }; + let Ok(public) = secp256k1::PublicKey::from_slice(&pubkey.0) else { + return false; + }; + let message_hash = sp_core_hashing::blake2_256(message.as_ref()); + let wrapped = Message::from_slice(&message_hash).expect("Message is 32 bytes; qed"); + signature.verify(&wrapped, &public).is_ok() +} + +/// An error handed back if creating a keypair fails. +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum Error { + /// Invalid seed. + #[error("Invalid seed (was it the wrong length?)")] + InvalidSeed, + /// Invalid seed. + #[error("Invalid seed for ECDSA, contained soft junction")] + SoftJunction, + /// Invalid phrase. + #[error("Cannot parse phrase: {0}")] + Phrase(#[from] bip39::Error), + /// Invalid hex. + #[error("Cannot parse hex string: {0}")] + Hex(#[from] hex::FromHexError), +} + +/// Dev accounts, helpful for testing but not to be used in production, +/// since the secret keys are known. +pub mod dev { + use super::*; + use std::str::FromStr; + + once_static_cloned! { + /// Equivalent to `{DEV_PHRASE}//Alice`. + pub fn alice() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Alice").unwrap()).unwrap() + } + /// Equivalent to `{DEV_PHRASE}//Bob`. + pub fn bob() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Bob").unwrap()).unwrap() + } + /// Equivalent to `{DEV_PHRASE}//Charlie`. + pub fn charlie() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Charlie").unwrap()).unwrap() + } + /// Equivalent to `{DEV_PHRASE}//Dave`. + pub fn dave() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Dave").unwrap()).unwrap() + } + /// Equivalent to `{DEV_PHRASE}//Eve`. + pub fn eve() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Eve").unwrap()).unwrap() + } + /// Equivalent to `{DEV_PHRASE}//Ferdie`. + pub fn ferdie() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Ferdie").unwrap()).unwrap() + } + /// Equivalent to `{DEV_PHRASE}//One`. + pub fn one() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//One").unwrap()).unwrap() + } + /// Equivalent to `{DEV_PHRASE}//Two`. + pub fn two() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Two").unwrap()).unwrap() + } + } +} + +// Make `Keypair` usable to sign transactions in Subxt. This is optional so that +// `subxt-signer` can be used entirely independently of Subxt. +#[cfg(feature = "subxt")] +mod subxt_compat { + use super::*; + + use subxt::config::Config; + use subxt::tx::Signer as SignerT; + use subxt::utils::{AccountId32, MultiAddress, MultiSignature}; + + impl From for MultiSignature { + fn from(value: Signature) -> Self { + MultiSignature::Ecdsa(value.0) + } + } + + impl From for AccountId32 { + fn from(value: PublicKey) -> Self { + value.to_account_id() + } + } + + impl From for MultiAddress { + fn from(value: PublicKey) -> Self { + value.to_address() + } + } + + impl PublicKey { + /// A shortcut to obtain an [`AccountId32`] from a [`PublicKey`]. + /// We often want this type, and using this method avoids any + /// ambiguous type resolution issues. + pub fn to_account_id(self) -> AccountId32 { + AccountId32(sp_core_hashing::blake2_256(&self.0)) + } + /// A shortcut to obtain a [`MultiAddress`] from a [`PublicKey`]. + /// We often want this type, and using this method avoids any + /// ambiguous type resolution issues. + pub fn to_address(self) -> MultiAddress { + MultiAddress::Id(self.to_account_id()) + } + } + + impl SignerT for Keypair + where + T::AccountId: From, + T::Address: From, + T::Signature: From, + { + fn account_id(&self) -> T::AccountId { + self.public_key().into() + } + + fn address(&self) -> T::Address { + self.public_key().into() + } + + fn sign(&self, signer_payload: &[u8]) -> T::Signature { + self.sign(signer_payload).into() + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use super::*; + + use sp_core::crypto::Pair as _; + use sp_core::ecdsa::Pair as SpPair; + + #[test] + fn check_from_phrase_matches() { + for _ in 0..20 { + let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None); + let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected"); + let pair = Keypair::from_phrase(&phrase, None).expect("should be valid"); + + assert_eq!(sp_pair.public().0, pair.public_key().0); + } + } + + #[test] + fn check_from_phrase_with_password_matches() { + for _ in 0..20 { + let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing")); + let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected"); + let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid"); + + assert_eq!(sp_pair.public().0, pair.public_key().0); + } + } + + #[test] + fn check_from_secret_uri_matches() { + // Some derive junctions to check that the logic there aligns: + let uri_paths = ["//bar", "//0001", "//1", "//0001", "//foo//bar//wibble"]; + + for i in 0..2 { + for path in &uri_paths { + // Build an sp_core::Pair that includes a phrase, path and password: + let password = format!("Testing{i}"); + let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some(&password)); + let uri = format!("{phrase}{path}///{password}"); + let sp_pair = SpPair::from_string(&uri, None).expect("should be valid"); + + // Now build a local Keypair using the equivalent API: + let uri = SecretUri::from_str(&uri).expect("should be valid secret URI"); + let pair = Keypair::from_uri(&uri).expect("should be valid"); + + // They should match: + assert_eq!(sp_pair.public().0, pair.public_key().0); + } + } + } + + #[test] + fn check_derive_errs_with_soft_junction() { + let uri_paths = ["/bar", "/1", "//foo//bar/wibble"]; + for path in &uri_paths { + let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None); + let uri = format!("{phrase}{path}"); + let uri = SecretUri::from_str(&uri).expect("should be valid secret URI"); + let result = Keypair::from_uri(&uri); + assert_eq!(result.err(), Some(Error::SoftJunction)); + } + } + + #[test] + fn check_signing_and_verifying_matches() { + use sp_core::ecdsa::Signature as SpSignature; + + for _ in 0..20 { + let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing")); + let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected"); + let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid"); + + let message = b"Hello world"; + let sp_sig = sp_pair.sign(message).0; + let sig = pair.sign(message).0; + + assert!(SpPair::verify( + &SpSignature(sig), + message, + &sp_pair.public(), + )); + assert!(verify(&Signature(sp_sig), message, &pair.public_key())); + } + } + + #[test] + fn check_hex_uris() { + // Hex URIs seem to ignore the password on sp_core and here. Check that this is consistent. + let uri_str = + "0x1122334455667788112233445566778811223344556677881122334455667788///SomePassword"; + + let uri = SecretUri::from_str(uri_str).expect("should be valid"); + let pair = Keypair::from_uri(&uri).expect("should be valid"); + let sp_pair = SpPair::from_string(uri_str, None).expect("should be valid"); + + assert_eq!(pair.public_key().0, sp_pair.public().0); + } +} diff --git a/signer/src/lib.rs b/signer/src/lib.rs index 1598c364a1..846d95d36b 100644 --- a/signer/src/lib.rs +++ b/signer/src/lib.rs @@ -20,8 +20,13 @@ mod utils; mod crypto; // An sr25519 key pair implementation. +#[cfg(feature = "sr25519")] pub mod sr25519; +// An ecdsa key pair implementation. +#[cfg(feature = "ecdsa")] +pub mod ecdsa; + // Re-export useful bits and pieces for generating a Pair from a phrase, // namely the Mnemonic struct. pub use bip39; diff --git a/signer/src/utils.rs b/signer/src/utils.rs index 200392f723..2e12665419 100644 --- a/signer/src/utils.rs +++ b/signer/src/utils.rs @@ -2,6 +2,8 @@ // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. +#![allow(unused_macros)] + /// Use like: /// /// ```rust,ignore diff --git a/signer/wasm-tests/Cargo.toml b/signer/wasm-tests/Cargo.toml index ac92982240..008dc84018 100644 --- a/signer/wasm-tests/Cargo.toml +++ b/signer/wasm-tests/Cargo.toml @@ -14,7 +14,7 @@ serde_json = "1" # enable the "web" feature here but don't want it enabled as part # of workspace builds. Also disable the "subxt" feature here because # we want to ensure it works in isolation of that. -subxt-signer = { path = "..", default-features = false, features = ["web"] } +subxt-signer = { path = "..", default-features = false, features = ["web", "sr25519", "ecdsa"] } # this shouldn't be needed, it's in workspace.exclude, but still # I get the complaint unless I add it... diff --git a/signer/wasm-tests/tests/wasm.rs b/signer/wasm-tests/tests/wasm.rs index e393b285a0..d5cf4227ba 100644 --- a/signer/wasm-tests/tests/wasm.rs +++ b/signer/wasm-tests/tests/wasm.rs @@ -1,6 +1,6 @@ #![cfg(target_arch = "wasm32")] -use subxt_signer::sr25519; +use subxt_signer::{ ecdsa, sr25519 }; use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -16,7 +16,7 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); // (subxt seems to, for instance). #[wasm_bindgen_test] -async fn wasm_signing_works() { +async fn wasm_sr25519_signing_works() { let alice = sr25519::dev::alice(); // There's some non-determinism in the signing, so this ensures that @@ -24,3 +24,13 @@ async fn wasm_signing_works() { let signature = alice.sign(b"Hello there"); assert!(sr25519::verify(&signature, b"Hello there", &alice.public_key())); } + +#[wasm_bindgen_test] +async fn wasm_ecdsa_signing_works() { + let alice = ecdsa::dev::alice(); + + // There's some non-determinism in the signing, so this ensures that + // the rand stuff is configured properly to run ok in wasm. + let signature = alice.sign(b"Hello there"); + assert!(ecdsa::verify(&signature, b"Hello there", &alice.public_key())); +} \ No newline at end of file diff --git a/subxt/src/book/usage/transactions.rs b/subxt/src/book/usage/transactions.rs index ab560bd54d..d4b3ce75d4 100644 --- a/subxt/src/book/usage/transactions.rs +++ b/subxt/src/book/usage/transactions.rs @@ -65,7 +65,7 @@ //! //! There are two main ways to create a compatible signer instance: //! 1. The `subxt_signer` crate provides a WASM compatible implementation of [`crate::tx::Signer`] -//! for chains which require sr25519 signatures (requires the `subxt` feature to be enabled). +//! for chains which require sr25519 or ecdsa signatures (requires the `subxt` feature to be enabled). //! 2. Alternately, Subxt can use instances of Substrate's [`sp_core::Pair`] to sign things by wrapping //! them in a [`crate::tx::PairSigner`] (requires the `substrate-compat` feature to be enabled). //! @@ -104,9 +104,16 @@ //! let keypair = sp_core::sr25519::Pair::from_string("vessel ladder alter error federal sibling chat ability sun glass valve picture/0/1///Password", None) //! .expect("valid URI"); //! let keypair = PairSigner::::new(keypair); +//! # +//! # // Test that these all impl Signer trait while we're here: +//! # +//! # fn is_subxt_signer(_signer: impl subxt::tx::Signer) {} +//! # is_subxt_signer(subxt_signer::sr25519::dev::alice()); +//! # is_subxt_signer(subxt_signer::ecdsa::dev::alice()); +//! # is_subxt_signer(PairSigner::::new(sp_keyring::AccountKeyring::Alice.pair())); //! ``` //! -//! See the `subxt_signer::sr25519::Keypair` or the [`sp_core::Pair`] docs for more ways to construct +//! See the `subxt_signer` crate or the [`sp_core::Pair`] docs for more ways to construct //! and work with key pairs. //! //! If this isn't suitable/available, you can either implement [`crate::tx::Signer`] yourself to use