diff --git a/common/src/ethereum.rs b/common/src/ethereum.rs index 3d5ac4bb..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; @@ -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::{ContractCall, ExtendedMiddleware, ExtendedSigner}; use self::walletconnect::WalletConnect; /// Radicle's ENS domain. @@ -29,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#" @@ -49,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 { @@ -62,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()? { @@ -81,6 +88,9 @@ impl SignerOptions { Long("walletconnect") => { options.walletconnect = true; } + Long("legacy") => { + options.legacy = true; + } _ => unparsed.push(args::format(arg)), } } @@ -142,7 +152,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, } @@ -155,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 Signer 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!(), } } @@ -187,10 +227,10 @@ impl Signer 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!(), } } @@ -198,10 +238,10 @@ impl Signer 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), } } @@ -209,23 +249,59 @@ impl Signer for Wallet { &self, message: ðers::types::transaction::eip2718::TypedTransaction, ) -> Result { + 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.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(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), + 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()) @@ -252,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) } } } @@ -260,8 +342,9 @@ impl Wallet { pub async fn transaction(call: ContractCall) -> anyhow::Result where D: Detokenize, - M: Middleware + 'static, + M: ExtendedMiddleware + 'static, { + 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 { @@ -318,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 new file mode 100644 index 00000000..07bfabbb --- /dev/null +++ b/common/src/ethereum/signer.rs @@ -0,0 +1,157 @@ +use ethers::abi::Detokenize; +use ethers::prelude::signer::SignerMiddlewareError; +use ethers::prelude::*; +use ethers::types::transaction::eip2718::TypedTransaction; + +use std::fmt::Debug; +use std::sync::Arc; + +use async_trait::async_trait; + +/// Trait for sending transactions +/// +/// 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 ExtendedSigner: Debug + Send + Sync + Signer { + /// Sends the transaction (only applicable for walletconnect) + async fn send_transaction(&self, message: &TypedTransaction) -> Result; + + /// Check if signer is a WalletConnect signer + fn is_walletconnect(&self) -> bool; + + /// Check if signer is a legacy signer + fn is_legacy(&self) -> bool; +} + +#[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: T, + block: Option, + ) -> Result, Self::Error>; +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ExtendedMiddleware for SignerMiddleware +where + M: Middleware, + S: ExtendedSigner, +{ + /// 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.signer().is_walletconnect() && !self.signer().is_legacy() { + let tx_hash = self + .signer() + .send_transaction(&tx) + .await + .map_err(SignerMiddlewareError::SignerError)?; + 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 + // + // Return the raw rlp-encoded signed transaction + let signed_tx = tx.rlp_signed(self.signer().chain_id(), &signature); + + // Submit the raw transaction + self.inner() + .send_raw_transaction(signed_tx) + .await + .map_err(SignerMiddlewareError::MiddlewareError) + } + } +} + +#[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(ContractError::MiddlewareError) + } +} diff --git a/common/src/ethereum/walletconnect.rs b/common/src/ethereum/walletconnect.rs index df05b1a4..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 { @@ -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,147 @@ 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), }) } + + 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])> { + 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()); + } } diff --git a/ens/src/lib.rs b/ens/src/lib.rs index 21b5d2b1..caeee9a8 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; @@ -153,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)?, } @@ -186,13 +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 { @@ -287,30 +290,34 @@ 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..."); - 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:"); diff --git a/ens/src/resolver.rs b/ens/src/resolver.rs index 29504d29..844b4231 100644 --- a/ens/src/resolver.rs +++ b/ens/src/resolver.rs @@ -5,11 +5,13 @@ 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; +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 c322b15c..506bd2b6 100644 --- a/gov/src/governance.rs +++ b/gov/src/governance.rs @@ -5,13 +5,15 @@ 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; +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 4f7f5f39..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, SignerMiddleware}; +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::{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?;