From 0e67a2361e2ba2ceff98f2aec623d4f99618077b Mon Sep 17 00:00:00 2001 From: xphoniex Date: Mon, 2 May 2022 11:44:59 +0000 Subject: [PATCH 1/6] rad-ens: Don't specify gas for contract call Signed-off-by: xphoniex --- ens/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ens/src/lib.rs b/ens/src/lib.rs index 21b5d2b1..4f27d25e 100644 --- a/ens/src/lib.rs +++ b/ens/src/lib.rs @@ -287,7 +287,7 @@ async fn setup( } } - let call = resolver.multicall(calls)?.gas(21000); + let call = resolver.multicall(calls)?; ethereum::transaction(call).await?; let spinner = term::spinner("Updating local identity..."); From 9ce3c1dbb48eb1d405a966a3d946971656efd6b2 Mon Sep 17 00:00:00 2001 From: xphoniex Date: Tue, 3 May 2022 04:36:14 +0000 Subject: [PATCH 2/6] rad-ens: Only update local identity for mainnet Signed-off-by: xphoniex --- ens/src/lib.rs | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/ens/src/lib.rs b/ens/src/lib.rs index 4f27d25e..925fb5ba 100644 --- a/ens/src/lib.rs +++ b/ens/src/lib.rs @@ -2,7 +2,7 @@ use std::ffi::OsString; use anyhow::anyhow; -use ethers::prelude::{Address, Http, Provider, SignerMiddleware}; +use ethers::prelude::{Address, Chain, Http, Provider, Signer, SignerMiddleware}; use librad::git::identities::local::LocalIdentity; use librad::git::Storage; @@ -190,6 +190,7 @@ async fn setup( storage: &Storage, ) -> anyhow::Result<()> { let urn = id.urn(); + let chain_id = signer.chain_id(); let signer = SignerMiddleware::new(provider, signer); let radicle_name = name.ends_with(ethereum::RADICLE_DOMAIN); let resolver = match PublicResolver::get(name, signer).await { @@ -290,27 +291,31 @@ async fn setup( let call = resolver.multicall(calls)?; ethereum::transaction(call).await?; - let spinner = term::spinner("Updating local identity..."); - match person::set_ens_payload( - person::Ens { - name: name.to_owned(), - }, - storage, - ) { - Ok(doc) => { - spinner.finish(); - term::blob(serde_json::to_string(&doc.payload())?); - } - Err(err) => { - spinner.failed(); - return Err(err); + if chain_id == u64::from(Chain::Mainnet) { + let spinner = term::spinner("Updating local identity..."); + match person::set_ens_payload( + person::Ens { + name: name.to_owned(), + }, + storage, + ) { + Ok(doc) => { + spinner.finish(); + term::blob(serde_json::to_string(&doc.payload())?); + } + Err(err) => { + spinner.failed(); + return Err(err); + } } - } - term::info!( - "Successfully associated local 🌱 identity with {}", - term::format::highlight(name) - ); + term::info!( + "Successfully associated local 🌱 identity with {}", + term::format::highlight(name) + ); + } else { + term::warning("Warning: Skipping local ENS setup"); + } term::blank(); term::tip!("To view your profile, visit:"); From 05a653a76d03f44626228993bd738c5cfb50d781 Mon Sep 17 00:00:00 2001 From: xphoniex Date: Tue, 3 May 2022 04:42:14 +0000 Subject: [PATCH 3/6] common: Make WalletConnect txs legacy Signed-off-by: xphoniex --- common/src/ethereum.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/src/ethereum.rs b/common/src/ethereum.rs index 3d5ac4bb..17cabf5c 100644 --- a/common/src/ethereum.rs +++ b/common/src/ethereum.rs @@ -80,6 +80,7 @@ impl SignerOptions { } Long("walletconnect") => { options.walletconnect = true; + std::env::set_var("RAD_SIGNER_LEGACY", "true"); } _ => unparsed.push(args::format(arg)), } @@ -262,6 +263,11 @@ where D: Detokenize, M: Middleware + 'static, { + let call = if std::env::var_os("RAD_SIGNER_LEGACY").is_some() { + call.legacy() + } else { + call + }; let receipt = loop { let spinner = term::spinner("Waiting for transaction to be signed..."); let tx = match call.send().await { From 2f81a397090f408b5d26070bd3588510ae18edae Mon Sep 17 00:00:00 2001 From: xphoniex Date: Mon, 2 May 2022 13:09:10 +0000 Subject: [PATCH 4/6] common: improve WalletConnect * add gas limit * remove nonce (could cause issue) * don't transform `v` as it's legacy * improve signature extraction * add error for when WalletConnect API failed to sign Signed-off-by: xphoniex --- common/src/ethereum.rs | 2 +- common/src/ethereum/walletconnect.rs | 134 ++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/common/src/ethereum.rs b/common/src/ethereum.rs index 17cabf5c..b5228402 100644 --- a/common/src/ethereum.rs +++ b/common/src/ethereum.rs @@ -143,7 +143,7 @@ pub enum WalletError { #[error(transparent)] Local(#[from] ethers::signers::WalletError), #[error(transparent)] - WalletConnect(#[from] ::walletconnect::client::CallError), + WalletConnect(#[from] walletconnect::WalletError), #[error("no wallet specified")] NoWallet, } diff --git a/common/src/ethereum/walletconnect.rs b/common/src/ethereum/walletconnect.rs index df05b1a4..e7f2a11a 100644 --- a/common/src/ethereum/walletconnect.rs +++ b/common/src/ethereum/walletconnect.rs @@ -12,6 +12,14 @@ pub struct WalletConnect { address: Address, } +#[derive(Debug, thiserror::Error)] +pub enum WalletError { + #[error(transparent)] + Call(#[from] CallError), + #[error("failed to sign the tx")] + TransactionSignature, +} + impl WalletConnect { pub fn new() -> Result> { let client = Client::new( @@ -61,14 +69,15 @@ impl WalletConnect { pub async fn sign_message>( &self, msg: S, - ) -> Result { + ) -> Result { let msg = unsafe { std::str::from_utf8_unchecked(msg.as_ref()) }; self.client .personal_sign(&[msg, &self.address_string()]) .await + .map_err(WalletError::from) } - pub async fn sign_transaction(&self, msg: &TypedTransaction) -> Result { + pub async fn sign_transaction(&self, msg: &TypedTransaction) -> Result { let to = if let Some(NameOrAddress::Address(address)) = msg.to() { Some(*address) } else { @@ -77,28 +86,125 @@ impl WalletConnect { let tx = Transaction { from: *msg.from().unwrap(), to, - gas_limit: None, + gas_limit: msg.gas().cloned(), gas_price: msg.gas_price(), value: *msg.value().unwrap_or(&U256::from(0)), data: msg.data().unwrap().to_vec(), - nonce: msg.nonce().copied(), + nonce: None, }; let raw = self.client.sign_transaction(tx).await?.to_vec(); - assert_eq!(raw[raw.len() - 66], 160); - assert_eq!(raw[raw.len() - 33], 160); - - // Transform `v` according to: - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#specification - let mut v = raw[raw.len() - 67] as u64; - if v == 27 || v == 28 { - v += 2 * self.chain_id() + 8; + let mut v_r_s = None; + for offset in 0..7 { + let mut head = raw.len() - 67 + offset; + v_r_s = extract_v_r_s(&raw[head..]); + if v_r_s.is_some() { + break; + } + + if offset == 0 { + continue; + } + head = raw.len() - 67 - offset; + v_r_s = extract_v_r_s(&raw[head..]); + if v_r_s.is_some() { + break; + } } + let (v, r, s) = v_r_s.ok_or(WalletError::TransactionSignature)?; Ok(Signature { v, - r: U256::from(&raw[raw.len() - 65..raw.len() - 33]), - s: U256::from(&raw[raw.len() - 32..]), + r: U256::from(r), + s: U256::from(s), }) } } + +fn extract_v_r_s(tx: &[u8]) -> Option<(u64, &[u8], &[u8])> { + let mut head = 0_usize; + let v: u64 = tx[head].into(); + + head += 1; + if tx[head] <= 0x80 { + return None; + } + let len_r = (tx[head] - 0x80) as usize; + if head + len_r >= tx.len() { + return None; + } + let r = &tx[head + 1..head + 1 + len_r]; + + head += 1 + len_r; + if tx[head] <= 0x80 { + return None; + } + let len_s = (tx[head] - 0x80) as usize; + if head + len_s >= tx.len() { + return None; + } + let s = &tx[head + 1..head + 1 + len_s]; + + if 1 + r.len() + s.len() + 2 != tx.len() { + return None; + } + + Some((v, r, s)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_regular_sig() { + let tx = [ + 0x1c, 0xa0, 0x88, 0xff, 0x6c, 0xf0, 0xfe, 0xfd, 0x94, 0xdb, 0x46, 0x11, 0x11, 0x49, + 0xae, 0x4b, 0xfc, 0x17, 0x9e, 0x9b, 0x94, 0x72, 0x1f, 0xff, 0xd8, 0x21, 0xd3, 0x8d, + 0x16, 0x46, 0x4b, 0x3f, 0x71, 0xd0, 0xa0, 0x45, 0xe0, 0xaf, 0xf8, 0x00, 0x96, 0x1c, + 0xfc, 0xe8, 0x05, 0xda, 0xef, 0x70, 0x16, 0xb9, 0xb6, 0x75, 0xc1, 0x37, 0xa6, 0xa4, + 0x1a, 0x54, 0x8f, 0x7b, 0x60, 0xa3, 0x48, 0x4c, 0x06, 0xa3, 0x3a, + ]; + + let v_r_s = extract_v_r_s(&tx); + assert!(v_r_s.is_some()); + let (v, r, s) = v_r_s.unwrap(); + + assert_eq!(v, 0x1c); + assert_eq!(r, &tx[tx.len() - 65..tx.len() - 33]); + assert_eq!(s, &tx[tx.len() - 32..]); + } + + #[test] + fn test_variable_sig() { + let tx = [ + 0x2c, 0xa0, 0x09, 0x0c, 0x0a, 0x25, 0xaf, 0x16, 0x3b, 0x51, 0x86, 0xd5, 0x6f, 0x61, + 0xd2, 0xd1, 0xe7, 0xcf, 0xf1, 0x05, 0xb8, 0x9e, 0x24, 0xed, 0x48, 0x26, 0x7c, 0x43, + 0xa0, 0x22, 0x27, 0xd9, 0xf7, 0x14, 0x9f, 0x9b, 0xcc, 0xf7, 0x3a, 0xef, 0xa7, 0x7d, + 0x2c, 0xcb, 0x0b, 0x81, 0x59, 0x15, 0x04, 0xde, 0xcc, 0x07, 0xc1, 0x26, 0x92, 0xf9, + 0x0f, 0xfe, 0x47, 0xd0, 0xf0, 0xbd, 0xea, 0x99, 0xa6, 0x8d, + ]; + + let v_r_s = extract_v_r_s(&tx); + assert!(v_r_s.is_some()); + let (v, r, s) = v_r_s.unwrap(); + + assert_eq!(v, 0x2c); + assert_eq!(r, &tx[tx.len() - 64..tx.len() - 32]); + assert_eq!(s, &tx[tx.len() - 31..]); + } + + #[test] + fn test_malformed_sig() { + let tx = [ + 0x2c, 0xa0, 0x09, 0x0c, 0x0a, 0x25, 0xaf, 0x16, 0x3b, 0x51, 0x86, 0xd5, 0x6f, 0x61, + 0xd2, 0xd1, 0xe7, 0xcf, 0xf1, 0x05, 0xb8, 0x9e, 0x24, 0xed, 0x48, 0x26, 0x7c, 0x43, + 0xa0, 0x22, 0x27, 0xd9, 0xf7, 0x14, 0x81, 0x9b, 0xcc, 0xf7, 0x3a, 0xef, 0xa7, 0x7d, + 0x2c, 0xcb, 0x0b, 0x81, 0x59, 0x15, 0x04, 0xde, 0xcc, 0x07, 0xc1, 0x26, 0x92, 0xf9, + 0x0f, 0xfe, 0x47, 0xd0, 0xf0, 0xbd, 0xea, 0x99, 0xa6, 0x8d, + ]; + + let v_r_s = extract_v_r_s(&tx); + assert!(v_r_s.is_none()); + } +} From f199b3d1e22d97faa78490e0aa1a5edbb38ee623 Mon Sep 17 00:00:00 2001 From: xphoniex Date: Tue, 3 May 2022 17:46:12 +0000 Subject: [PATCH 5/6] Add support for EIP-1559 to WalletConnect Affects: * rad-gov * rad-ens Signed-off-by: xphoniex --- common/src/ethereum.rs | 26 ++- common/src/ethereum/signer.rs | 262 +++++++++++++++++++++++++++ common/src/ethereum/walletconnect.rs | 24 ++- ens/src/lib.rs | 7 +- ens/src/resolver.rs | 4 +- gov/src/governance.rs | 4 +- gov/src/lib.rs | 4 +- 7 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 common/src/ethereum/signer.rs diff --git a/common/src/ethereum.rs b/common/src/ethereum.rs index b5228402..557b839f 100644 --- a/common/src/ethereum.rs +++ b/common/src/ethereum.rs @@ -18,8 +18,11 @@ use ethers::types::Chain; use rad_terminal::args; use rad_terminal::components as term; +pub mod signer; + mod walletconnect; +use self::signer::Signer as ExtendedSigner; use self::walletconnect::WalletConnect; /// Radicle's ENS domain. @@ -80,6 +83,8 @@ impl SignerOptions { } Long("walletconnect") => { options.walletconnect = true; + } + Long("legacy") => { std::env::set_var("RAD_SIGNER_LEGACY", "true"); } _ => unparsed.push(args::format(arg)), @@ -157,7 +162,7 @@ pub enum Wallet { } #[async_trait::async_trait] -impl Signer for Wallet { +impl ExtendedSigner for Wallet { type Error = WalletError; fn chain_id(&self) -> u64 { @@ -216,6 +221,25 @@ impl Signer for Wallet { Self::WalletConnect(s) => s.sign_transaction(message).await.map_err(WalletError::from), } } + + async fn send_transaction( + &self, + message: ðers::types::transaction::eip2718::TypedTransaction, + ) -> Result { + match self { + Self::Ledger(_) => unimplemented!(), + Self::Local(_) => unimplemented!(), + Self::WalletConnect(s) => s.send_transaction(message).await.map_err(WalletError::from), + } + } + + fn is_walletconnect(&self) -> bool { + match self { + Self::Ledger(_) => false, + Self::Local(_) => false, + Self::WalletConnect(_) => true, + } + } } impl Wallet { diff --git a/common/src/ethereum/signer.rs b/common/src/ethereum/signer.rs new file mode 100644 index 00000000..841c8236 --- /dev/null +++ b/common/src/ethereum/signer.rs @@ -0,0 +1,262 @@ +use ethers::prelude::{maybe, PendingTransaction}; +use ethers::providers::{FromErr, Middleware}; +use ethers::types::transaction::eip2718::TypedTransaction; +use ethers::types::transaction::eip712::Eip712; +use ethers::types::{Address, BlockId, Bytes, Signature, H256}; + +use async_trait::async_trait; +use thiserror::Error; + +/// Trait for signing transactions and messages +/// +/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Signer: std::fmt::Debug + Send + Sync { + type Error: std::error::Error + Send + Sync; + /// Signs the hash of the provided message after prefixing it + async fn sign_message>( + &self, + message: S, + ) -> Result; + + /// Signs the transaction + async fn sign_transaction(&self, message: &TypedTransaction) -> Result; + + /// Sends the transaction (only applicable for walletconnect) + async fn send_transaction(&self, message: &TypedTransaction) -> Result; + + /// Encodes and signs the typed data according EIP-712. + /// Payload must implement Eip712 trait. + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result; + + /// Returns the signer's Ethereum Address + fn address(&self) -> Address; + + /// Returns the signer's chain id + fn chain_id(&self) -> u64; + + /// Sets the signer's chain id + fn with_chain_id>(self, chain_id: T) -> Self; + + /// Check if signer is a WalletConnect signer + fn is_walletconnect(&self) -> bool; +} + +#[derive(Debug)] +pub struct SignerMiddleware { + pub inner: M, + pub signer: S, + pub address: Address, + pub is_legacy: bool, + pub is_walletconnect: bool, +} + +impl FromErr for SignerMiddlewareError { + fn from(src: M::Error) -> SignerMiddlewareError { + SignerMiddlewareError::MiddlewareError(src) + } +} + +#[derive(Error, Debug)] +/// Error thrown when the client interacts with the blockchain +pub enum SignerMiddlewareError { + #[error("{0}")] + /// Thrown when the internal call to the signer fails + SignerError(S::Error), + + #[error("{0}")] + /// Thrown when an internal middleware errors + MiddlewareError(M::Error), + + /// Thrown if the `nonce` field is missing + #[error("no nonce was specified")] + NonceMissing, + /// Thrown if the `gas_price` field is missing + #[error("no gas price was specified")] + GasPriceMissing, + /// Thrown if the `gas` field is missing + #[error("no gas was specified")] + GasMissing, + /// Thrown if a signature is requested from a different address + #[error("specified from address is not signer")] + WrongSigner, +} + +// Helper functions for locally signing transactions +impl SignerMiddleware +where + M: Middleware, + S: Signer, +{ + /// Creates a new client from the provider and signer. + pub fn new(inner: M, signer: S) -> Self { + let is_legacy = std::env::var_os("RAD_SIGNER_LEGACY").is_some(); + let is_walletconnect = signer.is_walletconnect(); + let address = signer.address(); + SignerMiddleware { + inner, + signer, + address, + is_legacy, + is_walletconnect, + } + } + + /// Signs and returns the RLP encoding of the signed transaction + async fn sign_transaction( + &self, + tx: TypedTransaction, + ) -> Result> { + let signature = self + .signer + .sign_transaction(&tx) + .await + .map_err(SignerMiddlewareError::SignerError)?; + + // Return the raw rlp-encoded signed transaction + Ok(tx.rlp_signed(self.signer.chain_id(), &signature)) + } + + /// Returns the client's address + pub fn address(&self) -> Address { + self.address + } + + /// Returns a reference to the client's signer + pub fn signer(&self) -> &S { + &self.signer + } + + /* + pub fn with_signer(&self, signer: S) -> Self + where + S: Clone, + M: Clone, + { + let mut this = self.clone(); + this.address = signer.address(); + this.signer = signer; + this + } + */ +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Middleware for SignerMiddleware +where + M: Middleware, + S: Signer, +{ + type Error = SignerMiddlewareError; + type Provider = M::Provider; + type Inner = M; + + fn inner(&self) -> &M { + &self.inner + } + + /// Returns the client's address + fn default_sender(&self) -> Option
{ + Some(self.address) + } + + /// `SignerMiddleware` is instantiated with a signer. + async fn is_signer(&self) -> bool { + true + } + + async fn sign_transaction( + &self, + tx: &TypedTransaction, + _: Address, + ) -> Result { + Ok(self + .signer + .sign_transaction(tx) + .await + .map_err(SignerMiddlewareError::SignerError)?) + } + + /// Helper for filling a transaction's nonce using the wallet + async fn fill_transaction( + &self, + tx: &mut TypedTransaction, + block: Option, + ) -> Result<(), Self::Error> { + // get the `from` field's nonce if it's set, else get the signer's nonce + let from = if tx.from().is_some() && tx.from() != Some(&self.address()) { + *tx.from().unwrap() + } else { + self.address + }; + tx.set_from(from); + + let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?; + tx.set_nonce(nonce); + self.inner() + .fill_transaction(tx, block) + .await + .map_err(SignerMiddlewareError::MiddlewareError)?; + Ok(()) + } + + /// Signs and broadcasts the transaction. The optional parameter `block` can be passed so that + /// gas cost and nonce calculations take it into account. For simple transactions this can be + /// left to `None`. + async fn send_transaction + Send + Sync>( + &self, + tx: T, + block: Option, + ) -> Result, Self::Error> { + let mut tx = tx.into(); + + // fill any missing fields + self.fill_transaction(&mut tx, block).await?; + + // If the from address is set and is not our signer, delegate to inner + if tx.from().is_some() && tx.from() != Some(&self.address()) { + return self + .inner + .send_transaction(tx, block) + .await + .map_err(SignerMiddlewareError::MiddlewareError); + } + + if self.is_walletconnect && !self.is_legacy { + let tx_hash = self + .signer + .send_transaction(&tx) + .await + .map_err(SignerMiddlewareError::SignerError)?; + Ok(PendingTransaction::new(tx_hash, self.inner.provider())) + } else { + // if we have a nonce manager set, we should try handling the result in + // case there was a nonce mismatch + let signed_tx = self.sign_transaction(tx).await?; + + // Submit the raw transaction + self.inner + .send_raw_transaction(signed_tx) + .await + .map_err(SignerMiddlewareError::MiddlewareError) + } + } + + /// Signs a message with the internal signer, or if none is present it will make a call to + /// the connected node's `eth_call` API. + async fn sign + Send + Sync>( + &self, + data: T, + _: &Address, + ) -> Result { + self.signer + .sign_message(data.into()) + .await + .map_err(SignerMiddlewareError::SignerError) + } +} diff --git a/common/src/ethereum/walletconnect.rs b/common/src/ethereum/walletconnect.rs index e7f2a11a..587497d4 100644 --- a/common/src/ethereum/walletconnect.rs +++ b/common/src/ethereum/walletconnect.rs @@ -3,7 +3,7 @@ use walletconnect::client::{CallError, SessionError}; use walletconnect::{qr, Client, Metadata, Transaction}; use ethers::types::transaction::eip2718::TypedTransaction; -use ethers::types::{Address, NameOrAddress, Signature, U256}; +use ethers::types::{Address, NameOrAddress, Signature, H256, U256}; #[derive(Debug)] pub struct WalletConnect { @@ -119,6 +119,28 @@ impl WalletConnect { s: U256::from(s), }) } + + pub async fn send_transaction(&self, msg: &TypedTransaction) -> Result { + let to = if let Some(NameOrAddress::Address(address)) = msg.to() { + Some(*address) + } else { + None + }; + let tx = Transaction { + from: *msg.from().unwrap(), + to, + gas_limit: None, + gas_price: msg.gas_price(), + value: *msg.value().unwrap_or(&U256::from(0)), + data: msg.data().unwrap().to_vec(), + nonce: None, + }; + + self.client + .send_transaction(tx) + .await + .map_err(WalletError::from) + } } fn extract_v_r_s(tx: &[u8]) -> Option<(u64, &[u8], &[u8])> { diff --git a/ens/src/lib.rs b/ens/src/lib.rs index 925fb5ba..3783eb12 100644 --- a/ens/src/lib.rs +++ b/ens/src/lib.rs @@ -2,11 +2,14 @@ use std::ffi::OsString; use anyhow::anyhow; -use ethers::prelude::{Address, Chain, Http, Provider, Signer, SignerMiddleware}; +use ethers::prelude::{Address, Chain, Http, Provider}; use librad::git::identities::local::LocalIdentity; use librad::git::Storage; -use rad_common::ethereum::{ProviderOptions, SignerOptions}; +use rad_common::ethereum::{ + signer::{Signer, SignerMiddleware}, + ProviderOptions, SignerOptions, +}; use rad_common::{ethereum, keys, person, profile, seed}; use rad_terminal::args::{Args, Error, Help}; use rad_terminal::components as term; diff --git a/ens/src/resolver.rs b/ens/src/resolver.rs index 29504d29..9358d923 100644 --- a/ens/src/resolver.rs +++ b/ens/src/resolver.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use ethers::prelude::{signer::SignerMiddlewareError, Http, Middleware, ProviderError}; +use ethers::prelude::{Http, Middleware, ProviderError}; use ethers::types::{Address, Bytes}; use ethers::{ abi::{Abi, Detokenize, ParamType}, @@ -9,7 +9,7 @@ use ethers::{ providers::{ens::ENS_ADDRESS, Provider}, }; -use rad_common::ethereum; +use rad_common::ethereum::{self, signer::SignerMiddlewareError}; pub const RADICLE_ID_KEY: &str = "eth.radicle.id"; pub const RADICLE_SEED_ID_KEY: &str = "eth.radicle.seed.id"; diff --git a/gov/src/governance.rs b/gov/src/governance.rs index c322b15c..8e1aad5d 100644 --- a/gov/src/governance.rs +++ b/gov/src/governance.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use ethers::prelude::{signer::SignerMiddlewareError, Http, Middleware, ProviderError}; +use ethers::prelude::{Http, Middleware, ProviderError}; use ethers::types::{Address, U256}; use ethers::{ abi::Abi, @@ -11,7 +11,7 @@ use ethers::{ use std::str::FromStr; -use rad_common::ethereum; +use rad_common::ethereum::{self, signer::SignerMiddlewareError}; const RADICLE_GOVERNANCE_ADDRESS: &str = "0x690e775361AD66D1c4A25d89da9fCd639F5198eD"; const PUBLIC_RESOLVER_ABI: &str = diff --git a/gov/src/lib.rs b/gov/src/lib.rs index 4f7f5f39..c9e0be03 100644 --- a/gov/src/lib.rs +++ b/gov/src/lib.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use ethers::abi::token::{LenientTokenizer, Token, Tokenizer}; use ethers::abi::AbiParser; -use ethers::prelude::{Middleware, SignerMiddleware}; +use ethers::prelude::Middleware; use ethers::types::{Address, U256}; use anyhow::anyhow; @@ -12,7 +12,7 @@ use anyhow::Context; use regex::Regex; use rad_common::ethereum; -use rad_common::ethereum::{ProviderOptions, SignerOptions}; +use rad_common::ethereum::{signer::SignerMiddleware, ProviderOptions, SignerOptions}; use rad_terminal::args::{Args, Error, Help}; use rad_terminal::components as term; From 4f1ebebd0a3584734f9f4980180ca12586c6e1ca Mon Sep 17 00:00:00 2001 From: xphoniex Date: Sun, 8 May 2022 12:04:54 +0000 Subject: [PATCH 6/6] Remove legacy (tx) flag from env Signed-off-by: xphoniex --- common/src/ethereum.rs | 141 +++++++++++----- common/src/ethereum/signer.rs | 295 +++++++++++----------------------- ens/src/lib.rs | 15 +- ens/src/resolver.rs | 53 ++++-- gov/src/governance.rs | 64 +++++--- gov/src/lib.rs | 15 +- 6 files changed, 293 insertions(+), 290 deletions(-) diff --git a/common/src/ethereum.rs b/common/src/ethereum.rs index 557b839f..eca475c1 100644 --- a/common/src/ethereum.rs +++ b/common/src/ethereum.rs @@ -2,6 +2,7 @@ use std::convert::TryFrom; use std::env; use std::ffi::OsString; +use std::fmt::Debug; use std::path::PathBuf; use std::str::FromStr; @@ -10,7 +11,6 @@ use coins_bip32::path::DerivationPath; use anyhow::anyhow; use anyhow::Context as _; use ethers::abi::Detokenize; -use ethers::prelude::builders::ContractCall; use ethers::prelude::*; use ethers::types::transaction::eip712::Eip712; use ethers::types::Chain; @@ -22,7 +22,7 @@ pub mod signer; mod walletconnect; -use self::signer::Signer as ExtendedSigner; +use self::signer::{ContractCall, ExtendedMiddleware, ExtendedSigner}; use self::walletconnect::WalletConnect; /// Radicle's ENS domain. @@ -32,6 +32,7 @@ pub const SIGNER_OPTIONS: &str = r#" --ledger-hdpath Account derivation path when using a Ledger hardware device --keystore Keystore file containing encrypted private key (default: none) --walletconnect Use WalletConnect + --legacy Send transactions in legacy mode "#; pub const PROVIDER_OPTIONS: &str = r#" @@ -52,6 +53,8 @@ pub struct SignerOptions { pub keystore: Option, /// Walletconnect account (default: false). pub walletconnect: bool, + /// Legacy transaction (default: false). + pub legacy: bool, } impl SignerOptions { @@ -65,6 +68,7 @@ impl SignerOptions { .ok() .and_then(|v| DerivationPath::from_str(v.as_str()).ok()), walletconnect: false, + legacy: false, }; while let Some(arg) = parser.next()? { @@ -85,7 +89,7 @@ impl SignerOptions { options.walletconnect = true; } Long("legacy") => { - std::env::set_var("RAD_SIGNER_LEGACY", "true"); + options.legacy = true; } _ => unparsed.push(args::format(arg)), } @@ -161,31 +165,61 @@ pub enum Wallet { WalletConnect(WalletConnect), } +#[derive(Debug)] +pub enum TypedWallet { + Legacy(Wallet), + Modern(Wallet), +} + +impl TypedWallet { + fn wallet(&self) -> &Wallet { + match self { + Self::Legacy(wallet) => wallet, + Self::Modern(wallet) => wallet, + } + } + + fn own_wallet(self) -> Wallet { + match self { + Self::Legacy(wallet) => wallet, + Self::Modern(wallet) => wallet, + } + } + + fn wrapper(&self) -> fn(Wallet) -> Self { + match self { + Self::Legacy(_) => Self::Legacy, + Self::Modern(_) => Self::Modern, + } + } +} + #[async_trait::async_trait] -impl ExtendedSigner for Wallet { +impl Signer for TypedWallet { type Error = WalletError; fn chain_id(&self) -> u64 { - match self { - Self::Ledger(s) => s.chain_id(), - Self::Local(s) => s.chain_id(), - Self::WalletConnect(s) => s.chain_id(), + match self.wallet() { + Wallet::Ledger(s) => s.chain_id(), + Wallet::Local(s) => s.chain_id(), + Wallet::WalletConnect(s) => s.chain_id(), } } fn address(&self) -> Address { - match self { - Self::Ledger(s) => s.address(), - Self::Local(s) => s.address(), - Self::WalletConnect(s) => s.address(), + match self.wallet() { + Wallet::Ledger(s) => s.address(), + Wallet::Local(s) => s.address(), + Wallet::WalletConnect(s) => s.address(), } } fn with_chain_id>(self, chain_id: T) -> Self { - match self { - Self::Ledger(s) => Self::Ledger(s.with_chain_id(chain_id)), - Self::Local(s) => Self::Local(s.with_chain_id(chain_id)), - Self::WalletConnect(_s) => unimplemented!(), + let wrapper = self.wrapper(); + match self.own_wallet() { + Wallet::Ledger(s) => (wrapper)(Wallet::Ledger(s.with_chain_id(chain_id))), + Wallet::Local(s) => (wrapper)(Wallet::Local(s.with_chain_id(chain_id))), + Wallet::WalletConnect(_s) => unimplemented!(), } } @@ -193,10 +227,10 @@ impl ExtendedSigner for Wallet { &self, payload: &T, ) -> Result { - match self { - Self::Ledger(s) => s.sign_typed_data(payload).await.map_err(WalletError::from), - Self::Local(s) => s.sign_typed_data(payload).await.map_err(WalletError::from), - Self::WalletConnect(_s) => unimplemented!(), + match self.wallet() { + Wallet::Ledger(s) => s.sign_typed_data(payload).await.map_err(WalletError::from), + Wallet::Local(s) => s.sign_typed_data(payload).await.map_err(WalletError::from), + Wallet::WalletConnect(_s) => unimplemented!(), } } @@ -204,10 +238,10 @@ impl ExtendedSigner for Wallet { &self, message: S, ) -> Result { - match self { - Self::Ledger(s) => s.sign_message(message).await.map_err(WalletError::from), - Self::Local(s) => s.sign_message(message).await.map_err(WalletError::from), - Self::WalletConnect(s) => s.sign_message(message).await.map_err(WalletError::from), + match self.wallet() { + Wallet::Ledger(s) => s.sign_message(message).await.map_err(WalletError::from), + Wallet::Local(s) => s.sign_message(message).await.map_err(WalletError::from), + Wallet::WalletConnect(s) => s.sign_message(message).await.map_err(WalletError::from), } } @@ -215,42 +249,59 @@ impl ExtendedSigner for Wallet { &self, message: ðers::types::transaction::eip2718::TypedTransaction, ) -> Result { - match self { - Self::Ledger(s) => s.sign_transaction(message).await.map_err(WalletError::from), - Self::Local(s) => s.sign_transaction(message).await.map_err(WalletError::from), - Self::WalletConnect(s) => s.sign_transaction(message).await.map_err(WalletError::from), + match self.wallet() { + Wallet::Ledger(s) => s.sign_transaction(message).await.map_err(WalletError::from), + Wallet::Local(s) => s.sign_transaction(message).await.map_err(WalletError::from), + Wallet::WalletConnect(s) => { + s.sign_transaction(message).await.map_err(WalletError::from) + } } } +} +#[async_trait::async_trait] +impl ExtendedSigner for TypedWallet { async fn send_transaction( &self, message: ðers::types::transaction::eip2718::TypedTransaction, ) -> Result { - match self { - Self::Ledger(_) => unimplemented!(), - Self::Local(_) => unimplemented!(), - Self::WalletConnect(s) => s.send_transaction(message).await.map_err(WalletError::from), + match self.wallet() { + Wallet::Ledger(_) => unimplemented!(), + Wallet::Local(_) => unimplemented!(), + Wallet::WalletConnect(s) => { + s.send_transaction(message).await.map_err(WalletError::from) + } } } fn is_walletconnect(&self) -> bool { + match self.wallet() { + Wallet::Ledger(_) => false, + Wallet::Local(_) => false, + Wallet::WalletConnect(_) => true, + } + } + + fn is_legacy(&self) -> bool { match self { - Self::Ledger(_) => false, - Self::Local(_) => false, - Self::WalletConnect(_) => true, + Self::Legacy(_) => true, + Self::Modern(_) => false, } } } impl Wallet { /// Open a wallet from the given options and provider. - pub async fn open

(options: SignerOptions, provider: Provider

) -> anyhow::Result + pub async fn open

( + options: SignerOptions, + provider: Provider

, + ) -> anyhow::Result where P: JsonRpcClient + Clone + 'static, { let chain_id = provider.get_chainid().await?.as_u64(); - if let Some(keypath) = &options.keystore { + let wallet = if let Some(keypath) = &options.keystore { let password = term::secret_input_with_prompt("Keystore password"); let spinner = term::spinner("Decrypting keystore..."); let signer = LocalWallet::decrypt_keystore(keypath, password.unsecure()) @@ -277,6 +328,12 @@ impl Wallet { Ok(Wallet::WalletConnect(signer)) } else { Err(WalletError::NoWallet.into()) + }; + + if options.legacy { + wallet.map(TypedWallet::Legacy) + } else { + wallet.map(TypedWallet::Modern) } } } @@ -285,13 +342,9 @@ impl Wallet { pub async fn transaction(call: ContractCall) -> anyhow::Result where D: Detokenize, - M: Middleware + 'static, + M: ExtendedMiddleware + 'static, { - let call = if std::env::var_os("RAD_SIGNER_LEGACY").is_some() { - call.legacy() - } else { - call - }; + let call = call.set_tx_type(); let receipt = loop { let spinner = term::spinner("Waiting for transaction to be signed..."); let tx = match call.send().await { @@ -348,7 +401,7 @@ pub fn hex(bytes: impl AsRef<[u8]>) -> String { pub async fn get_wallet( signer_opts: SignerOptions, provider: Provider, -) -> anyhow::Result<(Wallet, Provider)> { +) -> anyhow::Result<(TypedWallet, Provider)> { use rad_terminal::args::Error; term::tip!("Accessing your wallet..."); diff --git a/common/src/ethereum/signer.rs b/common/src/ethereum/signer.rs index 841c8236..07bfabbb 100644 --- a/common/src/ethereum/signer.rs +++ b/common/src/ethereum/signer.rs @@ -1,210 +1,46 @@ -use ethers::prelude::{maybe, PendingTransaction}; -use ethers::providers::{FromErr, Middleware}; +use ethers::abi::Detokenize; +use ethers::prelude::signer::SignerMiddlewareError; +use ethers::prelude::*; use ethers::types::transaction::eip2718::TypedTransaction; -use ethers::types::transaction::eip712::Eip712; -use ethers::types::{Address, BlockId, Bytes, Signature, H256}; + +use std::fmt::Debug; +use std::sync::Arc; use async_trait::async_trait; -use thiserror::Error; -/// Trait for signing transactions and messages +/// Trait for sending transactions /// -/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. +/// Implement this trait to support WalletConnect in Leagcy and EIP-1559 mode. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait Signer: std::fmt::Debug + Send + Sync { - type Error: std::error::Error + Send + Sync; - /// Signs the hash of the provided message after prefixing it - async fn sign_message>( - &self, - message: S, - ) -> Result; - - /// Signs the transaction - async fn sign_transaction(&self, message: &TypedTransaction) -> Result; - +pub trait ExtendedSigner: Debug + Send + Sync + Signer { /// Sends the transaction (only applicable for walletconnect) async fn send_transaction(&self, message: &TypedTransaction) -> Result; - /// Encodes and signs the typed data according EIP-712. - /// Payload must implement Eip712 trait. - async fn sign_typed_data( - &self, - payload: &T, - ) -> Result; - - /// Returns the signer's Ethereum Address - fn address(&self) -> Address; - - /// Returns the signer's chain id - fn chain_id(&self) -> u64; - - /// Sets the signer's chain id - fn with_chain_id>(self, chain_id: T) -> Self; - /// Check if signer is a WalletConnect signer fn is_walletconnect(&self) -> bool; -} -#[derive(Debug)] -pub struct SignerMiddleware { - pub inner: M, - pub signer: S, - pub address: Address, - pub is_legacy: bool, - pub is_walletconnect: bool, + /// Check if signer is a legacy signer + fn is_legacy(&self) -> bool; } -impl FromErr for SignerMiddlewareError { - fn from(src: M::Error) -> SignerMiddlewareError { - SignerMiddlewareError::MiddlewareError(src) - } -} - -#[derive(Error, Debug)] -/// Error thrown when the client interacts with the blockchain -pub enum SignerMiddlewareError { - #[error("{0}")] - /// Thrown when the internal call to the signer fails - SignerError(S::Error), - - #[error("{0}")] - /// Thrown when an internal middleware errors - MiddlewareError(M::Error), - - /// Thrown if the `nonce` field is missing - #[error("no nonce was specified")] - NonceMissing, - /// Thrown if the `gas_price` field is missing - #[error("no gas price was specified")] - GasPriceMissing, - /// Thrown if the `gas` field is missing - #[error("no gas was specified")] - GasMissing, - /// Thrown if a signature is requested from a different address - #[error("specified from address is not signer")] - WrongSigner, -} - -// Helper functions for locally signing transactions -impl SignerMiddleware -where - M: Middleware, - S: Signer, -{ - /// Creates a new client from the provider and signer. - pub fn new(inner: M, signer: S) -> Self { - let is_legacy = std::env::var_os("RAD_SIGNER_LEGACY").is_some(); - let is_walletconnect = signer.is_walletconnect(); - let address = signer.address(); - SignerMiddleware { - inner, - signer, - address, - is_legacy, - is_walletconnect, - } - } - - /// Signs and returns the RLP encoding of the signed transaction - async fn sign_transaction( +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait ExtendedMiddleware: Sync + Send + Debug + Middleware { + async fn send_transaction + Send + Sync>( &self, - tx: TypedTransaction, - ) -> Result> { - let signature = self - .signer - .sign_transaction(&tx) - .await - .map_err(SignerMiddlewareError::SignerError)?; - - // Return the raw rlp-encoded signed transaction - Ok(tx.rlp_signed(self.signer.chain_id(), &signature)) - } - - /// Returns the client's address - pub fn address(&self) -> Address { - self.address - } - - /// Returns a reference to the client's signer - pub fn signer(&self) -> &S { - &self.signer - } - - /* - pub fn with_signer(&self, signer: S) -> Self - where - S: Clone, - M: Clone, - { - let mut this = self.clone(); - this.address = signer.address(); - this.signer = signer; - this - } - */ + tx: T, + block: Option, + ) -> Result, Self::Error>; } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Middleware for SignerMiddleware +impl ExtendedMiddleware for SignerMiddleware where M: Middleware, - S: Signer, + S: ExtendedSigner, { - type Error = SignerMiddlewareError; - type Provider = M::Provider; - type Inner = M; - - fn inner(&self) -> &M { - &self.inner - } - - /// Returns the client's address - fn default_sender(&self) -> Option

{ - Some(self.address) - } - - /// `SignerMiddleware` is instantiated with a signer. - async fn is_signer(&self) -> bool { - true - } - - async fn sign_transaction( - &self, - tx: &TypedTransaction, - _: Address, - ) -> Result { - Ok(self - .signer - .sign_transaction(tx) - .await - .map_err(SignerMiddlewareError::SignerError)?) - } - - /// Helper for filling a transaction's nonce using the wallet - async fn fill_transaction( - &self, - tx: &mut TypedTransaction, - block: Option, - ) -> Result<(), Self::Error> { - // get the `from` field's nonce if it's set, else get the signer's nonce - let from = if tx.from().is_some() && tx.from() != Some(&self.address()) { - *tx.from().unwrap() - } else { - self.address - }; - tx.set_from(from); - - let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?; - tx.set_nonce(nonce); - self.inner() - .fill_transaction(tx, block) - .await - .map_err(SignerMiddlewareError::MiddlewareError)?; - Ok(()) - } - /// Signs and broadcasts the transaction. The optional parameter `block` can be passed so that /// gas cost and nonce calculations take it into account. For simple transactions this can be /// left to `None`. @@ -221,42 +57,101 @@ where // If the from address is set and is not our signer, delegate to inner if tx.from().is_some() && tx.from() != Some(&self.address()) { return self - .inner + .inner() .send_transaction(tx, block) .await .map_err(SignerMiddlewareError::MiddlewareError); } - if self.is_walletconnect && !self.is_legacy { + if self.signer().is_walletconnect() && !self.signer().is_legacy() { let tx_hash = self - .signer + .signer() .send_transaction(&tx) .await .map_err(SignerMiddlewareError::SignerError)?; - Ok(PendingTransaction::new(tx_hash, self.inner.provider())) + Ok(PendingTransaction::new(tx_hash, self.inner().provider())) } else { + let signature = self + .signer() + .sign_transaction(&tx) + .await + .map_err(SignerMiddlewareError::SignerError)?; + // if we have a nonce manager set, we should try handling the result in // case there was a nonce mismatch - let signed_tx = self.sign_transaction(tx).await?; + // + // Return the raw rlp-encoded signed transaction + let signed_tx = tx.rlp_signed(self.signer().chain_id(), &signature); // Submit the raw transaction - self.inner + self.inner() .send_raw_transaction(signed_tx) .await .map_err(SignerMiddlewareError::MiddlewareError) } } +} - /// Signs a message with the internal signer, or if none is present it will make a call to - /// the connected node's `eth_call` API. - async fn sign + Send + Sync>( - &self, - data: T, - _: &Address, - ) -> Result { - self.signer - .sign_message(data.into()) +#[derive(Debug, Clone)] +pub struct ContractCall { + pub inner: ethers::prelude::builders::ContractCall, + pub client: Arc, + pub legacy: bool, +} + +impl ContractCall +where + M: ExtendedMiddleware, + D: Detokenize, +{ + /// Sets the type of transaction which is either Legacy or EIP-1559 + pub fn set_tx_type(mut self) -> Self { + if self.legacy { + self.inner = self.inner.legacy(); + } + + self + } + + /// Returns the underlying transaction's ABI encoded data + pub fn calldata(&self) -> Option { + self.inner.tx.data().cloned() + } + + /// Returns the estimated gas cost for the underlying transaction to be executed + pub async fn estimate_gas(&self) -> Result> { + self.client + .estimate_gas(&self.inner.tx) + .await + .map_err(ContractError::MiddlewareError) + } + + /// Queries the blockchain via an `eth_call` for the provided transaction. + /// + /// If executed on a non-state mutating smart contract function (i.e. `view`, `pure`) + /// then it will return the raw data from the chain. + /// + /// If executed on a mutating smart contract function, it will do a "dry run" of the call + /// and return the return type of the transaction without mutating the state + /// + /// Note: this function _does not_ send a transaction from your account + pub async fn call(&self) -> Result> { + let bytes = self + .client + .call(&self.inner.tx, self.inner.block) + .await + .map_err(ContractError::MiddlewareError)?; + + // decode output + let data = decode_function_data(&self.inner.function, &bytes, false)?; + + Ok(data) + } + + /// Signs and broadcasts the provided transaction + pub async fn send(&self) -> Result, ContractError> { + ExtendedMiddleware::send_transaction(&*self.client, self.inner.tx.clone(), self.inner.block) .await - .map_err(SignerMiddlewareError::SignerError) + .map_err(ContractError::MiddlewareError) } } diff --git a/ens/src/lib.rs b/ens/src/lib.rs index 3783eb12..caeee9a8 100644 --- a/ens/src/lib.rs +++ b/ens/src/lib.rs @@ -2,14 +2,11 @@ use std::ffi::OsString; use anyhow::anyhow; -use ethers::prelude::{Address, Chain, Http, Provider}; +use ethers::prelude::{Address, Chain, Http, Provider, Signer, SignerMiddleware}; use librad::git::identities::local::LocalIdentity; use librad::git::Storage; -use rad_common::ethereum::{ - signer::{Signer, SignerMiddleware}, - ProviderOptions, SignerOptions, -}; +use rad_common::ethereum::{ProviderOptions, SignerOptions}; use rad_common::{ethereum, keys, person, profile, seed}; use rad_terminal::args::{Args, Error, Help}; use rad_terminal::components as term; @@ -156,8 +153,9 @@ pub fn run(options: Options) -> anyhow::Result<()> { let name = term::text_input("ENS name", name)?; let provider = ethereum::provider(options.provider)?; let signer_opts = options.signer; + let legacy = signer_opts.legacy; let (wallet, provider) = rt.block_on(ethereum::get_wallet(signer_opts, provider))?; - rt.block_on(setup(&name, id, provider, wallet, &storage))?; + rt.block_on(setup(&name, id, provider, wallet, &storage, legacy))?; } Operation::SetLocal(name) => set_ens_payload(&name, &storage)?, } @@ -189,14 +187,15 @@ async fn setup( name: &str, id: LocalIdentity, provider: Provider, - signer: ethereum::Wallet, + signer: ethereum::TypedWallet, storage: &Storage, + legacy: bool, ) -> anyhow::Result<()> { let urn = id.urn(); let chain_id = signer.chain_id(); let signer = SignerMiddleware::new(provider, signer); let radicle_name = name.ends_with(ethereum::RADICLE_DOMAIN); - let resolver = match PublicResolver::get(name, signer).await { + let resolver = match PublicResolver::get(name, signer, legacy).await { Ok(resolver) => resolver, Err(err) => { if let resolver::Error::NameNotFound { .. } = err { diff --git a/ens/src/resolver.rs b/ens/src/resolver.rs index 9358d923..844b4231 100644 --- a/ens/src/resolver.rs +++ b/ens/src/resolver.rs @@ -1,15 +1,17 @@ use std::sync::Arc; -use ethers::prelude::{Http, Middleware, ProviderError}; +use ethers::prelude::{signer::SignerMiddlewareError, Http, Middleware, ProviderError}; use ethers::types::{Address, Bytes}; use ethers::{ abi::{Abi, Detokenize, ParamType}, contract::{AbiError, Contract, ContractError}, - prelude::builders::ContractCall, providers::{ens::ENS_ADDRESS, Provider}, }; -use rad_common::ethereum::{self, signer::SignerMiddlewareError}; +use rad_common::ethereum::{ + self, + signer::{ContractCall, ExtendedMiddleware}, +}; pub const RADICLE_ID_KEY: &str = "eth.radicle.id"; pub const RADICLE_SEED_ID_KEY: &str = "eth.radicle.seed.id"; @@ -22,6 +24,8 @@ const PUBLIC_RESOLVER_ABI: &str = include_str!(concat!( pub struct PublicResolver { contract: Contract, + legacy: bool, + client: Arc, } #[derive(thiserror::Error, Debug)] @@ -35,22 +39,31 @@ pub enum Error { #[error(transparent)] Abi(#[from] ethers::abi::Error), #[error(transparent)] - SignerMiddleware(#[from] SignerMiddlewareError, ethereum::Wallet>), + SignerMiddleware(#[from] SignerMiddlewareError, ethereum::TypedWallet>), } impl PublicResolver where - M: Middleware, + M: ExtendedMiddleware, Error: From<::Error>, { - pub fn new(addr: Address, client: impl Into>) -> Self { + pub fn new(addr: Address, client: impl Into>, legacy: bool) -> Self { let abi: Abi = serde_json::from_str(PUBLIC_RESOLVER_ABI).expect("The ABI is valid"); - let contract = Contract::new(addr, abi, client); + let client: Arc = client.into(); + let contract = Contract::new(addr, abi, Arc::clone(&client)); - Self { contract } + Self { + contract, + legacy, + client, + } } - pub async fn get(name: &str, client: impl Into>) -> Result> { + pub async fn get( + name: &str, + client: impl Into>, + legacy: bool, + ) -> Result> { let client = client.into(); let bytes = client .call( @@ -66,11 +79,13 @@ where name: name.to_owned(), }); } - Ok(Self::new(resolver, client)) + Ok(Self::new(resolver, client, legacy)) } pub fn multicall(&self, calls: Vec) -> Result>, AbiError> { - self.contract.method("multicall", calls) + self.contract + .method("multicall", calls) + .map(|tx| self.to_contract_call(tx)) } pub async fn text(&self, name: &str, key: &str) -> Result, Error> { @@ -106,7 +121,9 @@ where pub fn set_address(&self, name: &str, addr: Address) -> Result, AbiError> { let node = ethers::providers::ens::namehash(name); - self.contract.method("setAddr", (node, addr)) + self.contract + .method("setAddr", (node, addr)) + .map(|tx| self.to_contract_call(tx)) } pub fn set_text( @@ -119,5 +136,17 @@ where self.contract .method("setText", (node, key.to_owned(), val.to_owned())) + .map(|tx| self.to_contract_call(tx)) + } + + fn to_contract_call( + &self, + inner: ethers::contract::builders::ContractCall, + ) -> ContractCall { + ContractCall { + inner, + client: Arc::clone(&self.client), + legacy: self.legacy, + } } } diff --git a/gov/src/governance.rs b/gov/src/governance.rs index 8e1aad5d..506bd2b6 100644 --- a/gov/src/governance.rs +++ b/gov/src/governance.rs @@ -1,17 +1,19 @@ use std::sync::Arc; -use ethers::prelude::{Http, Middleware, ProviderError}; +use ethers::prelude::{signer::SignerMiddlewareError, Http, Middleware, ProviderError}; use ethers::types::{Address, U256}; use ethers::{ abi::Abi, contract::{AbiError, Contract, ContractError}, - prelude::builders::ContractCall, providers::Provider, }; use std::str::FromStr; -use rad_common::ethereum::{self, signer::SignerMiddlewareError}; +use rad_common::ethereum::{ + self, + signer::{ContractCall, ExtendedMiddleware}, +}; const RADICLE_GOVERNANCE_ADDRESS: &str = "0x690e775361AD66D1c4A25d89da9fCd639F5198eD"; const PUBLIC_RESOLVER_ABI: &str = @@ -19,6 +21,8 @@ const PUBLIC_RESOLVER_ABI: &str = pub struct Governance { contract: Contract, + legacy: bool, + client: Arc, } #[derive(thiserror::Error, Debug)] @@ -34,7 +38,7 @@ pub enum Error { #[error(transparent)] ContractAbi(#[from] AbiError), #[error(transparent)] - SignerMiddleware(#[from] SignerMiddlewareError, ethereum::Wallet>), + SignerMiddleware(#[from] SignerMiddlewareError, ethereum::TypedWallet>), } type Proposal = (Address, U256, U256, U256, U256, U256, bool, bool); @@ -60,15 +64,20 @@ impl std::fmt::Display for ProposalState { impl Governance where - M: Middleware, + M: ExtendedMiddleware, Error: From<::Error>, { - pub fn new(client: impl Into>) -> Self { + pub fn new(client: impl Into>, legacy: bool) -> Self { let abi: Abi = serde_json::from_str(PUBLIC_RESOLVER_ABI).expect("The ABI is valid"); let addr = Address::from_str(RADICLE_GOVERNANCE_ADDRESS).unwrap(); - let contract = Contract::new(addr, abi, client); + let client: Arc = client.into(); + let contract = Contract::new(addr, abi, Arc::clone(&client)); - Self { contract } + Self { + contract, + legacy, + client, + } } pub async fn get_proposal(&self, id: U256) -> Result> { @@ -106,7 +115,9 @@ where } pub fn cast_vote(&self, id: U256, support: bool) -> Result, AbiError> { - self.contract.method("castVote", (id, support)) + self.contract + .method("castVote", (id, support)) + .map(|tx| self.to_contract_call(tx)) } pub async fn queue_proposal(&self, id: U256) -> Result, Error> { @@ -119,6 +130,7 @@ where self.contract .method("queue", id) .map_err(Error::ContractAbi) + .map(|tx| self.to_contract_call(tx)) } pub async fn execute_proposal(&self, id: U256) -> Result, Error> { @@ -131,6 +143,7 @@ where self.contract .method("execute", id) .map_err(Error::ContractAbi) + .map(|tx| self.to_contract_call(tx)) } pub fn propose( @@ -143,15 +156,28 @@ where ) -> Result, AbiError> { use ethers::core::abi::Token; - self.contract.method( - "propose", - ( - Token::Array(targets.into_iter().map(Token::Address).collect()), - Token::Array(values.into_iter().map(Token::Uint).collect()), - Token::Array(signatures.into_iter().map(Token::String).collect()), - Token::Array(calldatas.into_iter().map(Token::Bytes).collect()), - description, - ), - ) + self.contract + .method( + "propose", + ( + Token::Array(targets.into_iter().map(Token::Address).collect()), + Token::Array(values.into_iter().map(Token::Uint).collect()), + Token::Array(signatures.into_iter().map(Token::String).collect()), + Token::Array(calldatas.into_iter().map(Token::Bytes).collect()), + description, + ), + ) + .map(|tx| self.to_contract_call(tx)) + } + + fn to_contract_call( + &self, + inner: ethers::contract::builders::ContractCall, + ) -> ContractCall { + ContractCall { + inner, + client: Arc::clone(&self.client), + legacy: self.legacy, + } } } diff --git a/gov/src/lib.rs b/gov/src/lib.rs index c9e0be03..113e0c97 100644 --- a/gov/src/lib.rs +++ b/gov/src/lib.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use ethers::abi::token::{LenientTokenizer, Token, Tokenizer}; use ethers::abi::AbiParser; -use ethers::prelude::Middleware; +use ethers::prelude::{signer::SignerMiddleware, Middleware}; use ethers::types::{Address, U256}; use anyhow::anyhow; @@ -12,7 +12,7 @@ use anyhow::Context; use regex::Regex; use rad_common::ethereum; -use rad_common::ethereum::{signer::SignerMiddleware, ProviderOptions, SignerOptions}; +use rad_common::ethereum::{signer::ExtendedMiddleware, ProviderOptions, SignerOptions}; use rad_terminal::args::{Args, Error, Help}; use rad_terminal::components as term; @@ -128,9 +128,10 @@ pub fn run(options: Options) -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; let provider = ethereum::provider(options.provider)?; let signer_opts = options.signer; + let legacy = signer_opts.legacy; let (wallet, provider) = rt.block_on(ethereum::get_wallet(signer_opts, provider))?; let signer = SignerMiddleware::new(provider, wallet); - let governance = Governance::new(signer); + let governance = Governance::new(signer, legacy); match options.command { Command::Execute { id } => { @@ -152,7 +153,7 @@ pub fn run(options: Options) -> anyhow::Result<()> { async fn run_execute(id: U256, governance: Governance) -> anyhow::Result<()> where - M: Middleware + 'static, + M: ExtendedMiddleware + 'static, crate::governance::Error: From<::Error>, { let call = governance.execute_proposal(id).await?; @@ -162,7 +163,7 @@ where async fn run_propose(file: OsString, governance: Governance) -> anyhow::Result<()> where - M: Middleware + 'static, + M: ExtendedMiddleware + 'static, crate::governance::Error: From<::Error>, { let spinner = term::spinner(&format!( @@ -245,7 +246,7 @@ where async fn run_queue(id: U256, governance: Governance) -> anyhow::Result<()> where - M: Middleware + 'static, + M: ExtendedMiddleware + 'static, crate::governance::Error: From<::Error>, { let call = governance.queue_proposal(id).await?; @@ -255,7 +256,7 @@ where async fn run_vote(id: U256, governance: Governance) -> anyhow::Result<()> where - M: Middleware + 'static, + M: ExtendedMiddleware + 'static, crate::governance::Error: From<::Error>, { let proposal = governance.get_proposal(id).await?;