Skip to content

Instantly share code, notes, and snippets.

@jsdw
Created March 7, 2024 17:40
Show Gist options
  • Save jsdw/13240f9341e433ea639b89d0d4235c8b to your computer and use it in GitHub Desktop.
Save jsdw/13240f9341e433ea639b89d0d4235c8b to your computer and use it in GitHub Desktop.
Subxt: An Ethereum compatible signer
[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"] }
// 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()
}
}
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