Created
March 7, 2024 17:40
-
-
Save jsdw/13240f9341e433ea639b89d0d4235c8b to your computer and use it in GitHub Desktop.
Subxt: An Ethereum compatible signer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "eth_signer_example" | |
version = "0.1.0" | |
edition = "2021" | |
[dependencies] | |
anyhow = { version = "1.0.80", features = ["backtrace"] } | |
subxt = "0.34.0" | |
tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } | |
codec = { package = "parity-scale-codec", version = "3.6.9", features = ["derive"] } | |
hex = "0.4.3" | |
keccak-hash = "0.10.0" | |
secp256k1 = { version = "0.28.2", features = ["recovery", "global-context"] } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 hex::FromHex; | |
use secp256k1::{ecdsa, Message, Keypair, SecretKey, SECP256K1}; | |
use keccak_hash::keccak; | |
#[derive(Debug)] | |
pub struct EthereumSigner(Keypair); | |
impl EthereumSigner { | |
pub fn from_private_key_hex(hex: &str) -> Result<EthereumSigner, anyhow::Error> { | |
let seed = <[u8; 32]>::from_hex(hex)?; | |
let secret = SecretKey::from_slice(&seed)?; | |
Ok(EthereumSigner(secp256k1::Keypair::from_secret_key( | |
SECP256K1, &secret, | |
))) | |
} | |
pub fn public_key(&self) -> secp256k1::PublicKey { | |
self.0.public_key() | |
} | |
pub fn account_id(&self) -> AccountId20 { | |
let uncompressed = self.0.public_key().serialize_uncompressed(); | |
let hash = keccak(&uncompressed[1..]).0; | |
let hash20 = hash[12..].try_into().expect("should be 20 bytes"); | |
AccountId20(hash20) | |
} | |
} | |
#[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode)] | |
pub struct EthereumSignature(pub [u8; 65]); | |
#[derive(Debug, Copy, Clone, codec::Encode)] | |
pub struct AccountId20(pub [u8; 20]); | |
impl AsRef<[u8]> for AccountId20 { | |
fn as_ref(&self) -> &[u8] { | |
&self.0 | |
} | |
} | |
impl <T: subxt::Config> subxt::tx::Signer<T> for EthereumSigner | |
where | |
T::AccountId: From<AccountId20>, | |
T::Address: From<AccountId20>, | |
T::Signature: From<EthereumSignature> | |
{ | |
fn account_id(&self) -> T::AccountId { | |
self.account_id().into() | |
} | |
fn address(&self) -> T::Address { | |
self.account_id().into() | |
} | |
fn sign(&self, signer_payload: &[u8]) -> T::Signature { | |
// The below is copied from subxt_signer's ecdsa signing; the only change | |
// is that we hash the message with keccak and not blake2_256, above, to | |
// match what the runtime verification does | |
let message_hash = keccak(signer_payload); | |
let wrapped = Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); | |
let recsig: ecdsa::RecoverableSignature = | |
SECP256K1.sign_ecdsa_recoverable(&wrapped, &self.0.secret_key()); | |
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; | |
EthereumSignature(signature_bytes).into() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mod eth_signer; | |
use subxt::OnlineClient; | |
use eth_signer::{EthereumSigner, EthereumSignature, AccountId20}; | |
#[subxt::subxt(runtime_metadata_insecure_url="ws://127.0.0.1:9933")] | |
mod eth_runtime {} | |
pub enum EthRuntimeConfig {} | |
impl subxt::Config for EthRuntimeConfig { | |
type Hash = subxt::utils::H256; | |
type AccountId = AccountId20; | |
type Address = AccountId20; | |
type Signature = EthereumSignature; | |
type Hasher = subxt::config::substrate::BlakeTwo256; | |
type Header = subxt::config::substrate::SubstrateHeader<u32, subxt::config::substrate::BlakeTwo256>; | |
type ExtrinsicParams = subxt::config::SubstrateExtrinsicParams<Self>; | |
type AssetId = u32; | |
} | |
// This helper makes it easy to use our `eth_signer::AccountId20`'s with generated | |
// code that expects a generated `eth_runtime::runtime_types::foo::AccountId20` type. | |
// an alternative is to do some type substitution in the generated code itself, but | |
// mostly I'd avoid doing that unless absolutely necessary. | |
impl From<eth_signer::AccountId20> for eth_runtime::runtime_types::foo::AccountId20 { | |
fn from(val: eth_signer::AccountId20) -> Self { | |
Self(val.0) | |
} | |
} | |
// public private | |
const ALITH: (&str, &str) = ("02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f", "5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133"); | |
const BALTHASAR: (&str, &str) = ("033bc19e36ff1673910575b6727a974a9abd80c9a875d41ab3e2648dbfb9e4b518", "8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b"); | |
#[tokio::main] | |
async fn main() -> Result<(), anyhow::Error> { | |
let api = OnlineClient::<EthRuntimeConfig>::from_insecure_url("ws://127.0.0.1:9933").await?; | |
let balthasar = EthereumSigner::from_private_key_hex(BALTHASAR.1)?; | |
let dest = balthasar.account_id(); | |
println!("balthasar pub: {}", hex::encode(&balthasar.public_key().serialize_uncompressed())); | |
println!("balthasar addr: {}", hex::encode(&dest)); | |
let balance_transfer_tx = eth_runtime::tx().balances().transfer_allow_death(dest.into(), 10_001); | |
let alith = EthereumSigner::from_private_key_hex(ALITH.1)?; | |
let events = api | |
.tx() | |
.sign_and_submit_then_watch_default(&balance_transfer_tx, &alith) | |
.await? | |
.wait_for_finalized_success() | |
.await?; | |
let transfer_event = events.find_first::<eth_runtime::balances::events::Transfer>()?; | |
if let Some(event) = transfer_event { | |
println!("Balance transfer success: {event:?}"); | |
} | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment