From ba5ae5a89478e32f56d30ef48f717ffc1105bdb7 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 17 Jun 2020 12:22:01 +0300 Subject: [PATCH] Add Celo support (#8) * feat(types): add optional Celo support * feat: add Celo feature flags to all crates * test(provider): add get_transaction celo test * test(signer): add send_transaction celo test * test(contract): add deploy and call contract function celo test --- .github/workflows/ci.yml | 5 + Cargo.lock | 72 ++++++ README.md | 21 +- ethers-contract/Cargo.toml | 3 + ethers-contract/src/factory.rs | 3 +- ethers-contract/tests/contract.rs | 233 +++++++++++++----- ethers-contract/tests/get_past_events.rs | 32 --- ethers-contract/tests/watch_events.rs | 38 --- ethers-core/Cargo.toml | 1 + .../src/types/chainstate/transaction.rs | 99 +++++++- ethers-core/src/types/crypto/keys.rs | 10 +- ethers-providers/Cargo.toml | 3 + ethers-providers/tests/provider.rs | 34 ++- ethers-signers/Cargo.toml | 3 + ethers-signers/tests/send_eth.rs | 66 ----- ethers-signers/tests/signer.rs | 98 ++++++++ ethers/Cargo.toml | 7 + 17 files changed, 521 insertions(+), 207 deletions(-) delete mode 100644 ethers-contract/tests/get_past_events.rs delete mode 100644 ethers-contract/tests/watch_events.rs delete mode 100644 ethers-signers/tests/send_eth.rs create mode 100644 ethers-signers/tests/signer.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbcd7021..49128872 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,11 @@ jobs: export PATH=$HOME/bin:$PATH cargo test + - name: cargo test (Celo) + run: | + export PATH=$HOME/bin:$PATH + cargo test --all-features + - name: cargo fmt run: cargo fmt --all -- --check diff --git a/Cargo.lock b/Cargo.lock index cd08ff04..292efc2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,15 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -290,6 +299,7 @@ dependencies = [ name = "ethers-contract" version = "0.1.0" dependencies = [ + "ethers", "ethers-contract-abigen", "ethers-contract-derive", "ethers-core", @@ -300,6 +310,7 @@ dependencies = [ "rustc-hex", "serde", "serde_json", + "serial_test", "thiserror", "tokio", ] @@ -774,6 +785,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.8" @@ -896,6 +916,30 @@ dependencies = [ "serde", ] +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.8", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1155,6 +1199,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sct" version = "0.6.0" @@ -1208,6 +1258,28 @@ dependencies = [ "url", ] +[[package]] +name = "serial_test" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef5f7c7434b2f2c598adc6f9494648a1e41274a75c0ba4056f680ae0c117fd6" +dependencies = [ + "lazy_static", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08338d8024b227c62bd68a12c7c9883f5c66780abaef15c550dc56f46ee6515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha2" version = "0.8.2" diff --git a/README.md b/README.md index facf414f..c5e55c79 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ #

ethers.rs

-**Complete Ethereum wallet implementation and utilities in Rust** +**Complete Ethereum and Celo wallet implementation and utilities in Rust** [![CircleCI](https://circleci.com/gh/circleci/circleci-docs.svg?style=svg)](https://circleci.com/gh/circleci/circleci-docs) @@ -20,6 +20,24 @@ ethers = { git = "github.com/gakonst/ethers-rs" } +### Celo Support + +[Celo](http://celo.org/) support is turned on via the feature-flag `celo`: + +```toml +[dependencies] + +ethers = { git = "github.com/gakonst/ethers-rs", features = ["celo"] } +``` + +Celo's transactions differ from Ethereum transactions by including 3 new fields: +- `fee_currency`: The currency fees are paid in (None for CELO, otherwise it's an Address) +- `gateway_fee_recipient`: The address of the fee recipient (None for no gateway fee paid) +- `gateway_fee`: Gateway fee amount (None for no gateway fee paid) + +The feature flag enables these additional fields in the transaction request builders and +in the transactions which are fetched over JSON-RPC. + ## Features - [x] Ethereum JSON-RPC Client @@ -28,6 +46,7 @@ ethers = { git = "github.com/gakonst/ethers-rs" } - [x] Querying past events - [x] Event monitoring as `Stream`s - [x] ENS as a first class citizen +- [x] Celo support - [ ] Websockets / `eth_subscribe` - [ ] Hardware Wallet Support - [ ] WASM Bindings diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index eb23e286..0677f9bc 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -19,8 +19,11 @@ once_cell = { version = "1.3.1", default-features = false } futures = "0.3.5" [dev-dependencies] +ethers = { version = "0.1.0", path = "../ethers" } tokio = { version = "0.2.21", default-features = false, features = ["macros"] } serde_json = "1.0.55" +serial_test = "0.4.0" [features] abigen = ["ethers-contract-abigen", "ethers-contract-derive"] +celo = ["ethers-core/celo", "ethers-core/celo", "ethers-providers/celo", "ethers-signers/celo"] diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index 9ac36269..0413dc41 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -10,9 +10,10 @@ use ethers_signers::{Client, Signer}; #[derive(Debug, Clone)] /// Helper which manages the deployment transaction of a smart contract pub struct Deployer<'a, P, S> { + /// The deployer's transaction, exposed for overriding the defaults + pub tx: TransactionRequest, abi: Abi, client: &'a Client, - tx: TransactionRequest, confs: usize, } diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 8e97e5b5..87174053 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -1,69 +1,190 @@ -use ethers_contract::ContractFactory; -use ethers_core::{ - types::{Address, H256}, - utils::Ganache, -}; +use ethers::{contract::ContractFactory, types::H256}; mod common; pub use common::*; -#[tokio::test] -async fn deploy_and_call_contract() { - let (abi, bytecode) = compile(); +#[cfg(not(feature = "celo"))] +mod eth_tests { + use super::*; + use ethers::{providers::StreamExt, types::Address, utils::Ganache}; + use serial_test::serial; - // launch ganache - let _ganache = Ganache::new() - .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") - .spawn(); + #[tokio::test] + #[serial] + async fn deploy_and_call_contract() { + let (abi, bytecode) = compile(); - // Instantiate the clients. We assume that clients consume the provider and the wallet - // (which makes sense), so for multi-client tests, you must clone the provider. - let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let client2 = connect("cc96601bc52293b53c4736a12af9130abf347669b3813f9ec4cafdf6991b087e"); + // launch ganache + let _ganache = Ganache::new() + .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") + .spawn(); - // create a factory which will be used to deploy instances of the contract - let factory = ContractFactory::new(abi, bytecode, &client); + // Instantiate the clients. We assume that clients consume the provider and the wallet + // (which makes sense), so for multi-client tests, you must clone the provider. + let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); + let client2 = connect("cc96601bc52293b53c4736a12af9130abf347669b3813f9ec4cafdf6991b087e"); - // `send` consumes the deployer so it must be cloned for later re-use - // (practically it's not expected that you'll need to deploy multiple instances of - // the _same_ deployer, so it's fine to clone here from a dev UX vs perf tradeoff) - let deployer = factory.deploy("initial value".to_string()).unwrap(); - let contract = deployer.clone().send().await.unwrap(); + // create a factory which will be used to deploy instances of the contract + let factory = ContractFactory::new(abi, bytecode, &client); - let get_value = contract.method::<_, String>("getValue", ()).unwrap(); - let last_sender = contract.method::<_, Address>("lastSender", ()).unwrap(); + // `send` consumes the deployer so it must be cloned for later re-use + // (practically it's not expected that you'll need to deploy multiple instances of + // the _same_ deployer, so it's fine to clone here from a dev UX vs perf tradeoff) + let deployer = factory.deploy("initial value".to_string()).unwrap(); + let contract = deployer.clone().send().await.unwrap(); - // the initial value must be the one set in the constructor - let value = get_value.clone().call().await.unwrap(); - assert_eq!(value, "initial value"); + let get_value = contract.method::<_, String>("getValue", ()).unwrap(); + let last_sender = contract.method::<_, Address>("lastSender", ()).unwrap(); - // make a call with `client2` - let _tx_hash = contract - .connect(&client2) - .method::<_, H256>("setValue", "hi".to_owned()) - .unwrap() - .send() - .await - .unwrap(); - assert_eq!(last_sender.clone().call().await.unwrap(), client2.address()); - assert_eq!(get_value.clone().call().await.unwrap(), "hi"); + // the initial value must be the one set in the constructor + let value = get_value.clone().call().await.unwrap(); + assert_eq!(value, "initial value"); - // we can also call contract methods at other addresses with the `at` call - // (useful when interacting with multiple ERC20s for example) - let contract2_addr = deployer.clone().send().await.unwrap().address(); - let contract2 = contract.at(contract2_addr); - let init_value: String = contract2 - .method::<_, String>("getValue", ()) - .unwrap() - .call() - .await - .unwrap(); - let init_address = contract2 - .method::<_, Address>("lastSender", ()) - .unwrap() - .call() - .await - .unwrap(); - assert_eq!(init_address, Address::zero()); - assert_eq!(init_value, "initial value"); + // make a call with `client2` + let _tx_hash = contract + .connect(&client2) + .method::<_, H256>("setValue", "hi".to_owned()) + .unwrap() + .send() + .await + .unwrap(); + assert_eq!(last_sender.clone().call().await.unwrap(), client2.address()); + assert_eq!(get_value.clone().call().await.unwrap(), "hi"); + + // we can also call contract methods at other addresses with the `at` call + // (useful when interacting with multiple ERC20s for example) + let contract2_addr = deployer.clone().send().await.unwrap().address(); + let contract2 = contract.at(contract2_addr); + let init_value: String = contract2 + .method::<_, String>("getValue", ()) + .unwrap() + .call() + .await + .unwrap(); + let init_address = contract2 + .method::<_, Address>("lastSender", ()) + .unwrap() + .call() + .await + .unwrap(); + assert_eq!(init_address, Address::zero()); + assert_eq!(init_value, "initial value"); + } + + #[tokio::test] + #[serial] + async fn get_past_events() { + let (abi, bytecode) = compile(); + let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); + let (_ganache, contract) = deploy(&client, abi, bytecode).await; + + // make a call with `client2` + let _tx_hash = contract + .method::<_, H256>("setValue", "hi".to_owned()) + .unwrap() + .send() + .await + .unwrap(); + + // and we can fetch the events + let logs: Vec = contract + .event("ValueChanged") + .unwrap() + .from_block(0u64) + .topic1(client.address()) // Corresponds to the first indexed parameter + .query() + .await + .unwrap(); + assert_eq!(logs[0].new_value, "initial value"); + assert_eq!(logs[1].new_value, "hi"); + assert_eq!(logs.len(), 2); + } + + #[tokio::test] + #[serial] + async fn watch_events() { + let (abi, bytecode) = compile(); + let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); + let (_ganache, contract) = deploy(&client, abi, bytecode).await; + + // We spawn the event listener: + let mut stream = contract + .event::("ValueChanged") + .unwrap() + .stream() + .await + .unwrap(); + + let num_calls = 3u64; + + // and we make a few calls + for i in 0..num_calls { + let _tx_hash = contract + .method::<_, H256>("setValue", i.to_string()) + .unwrap() + .send() + .await + .unwrap(); + } + + for i in 0..num_calls { + // unwrap the option of the stream, then unwrap the decoding result + let log = stream.next().await.unwrap().unwrap(); + assert_eq!(log.new_value, i.to_string()); + } + } +} + +#[cfg(feature = "celo")] +mod celo_tests { + use super::*; + use ethers::{ + providers::{Http, Provider}, + signers::Wallet, + }; + use std::convert::TryFrom; + + #[tokio::test] + async fn deploy_and_call_contract() { + let (abi, bytecode) = compile(); + + // Celo testnet + let provider = + Provider::::try_from("https://alfajores-forno.celo-testnet.org").unwrap(); + + // Funded with https://celo.org/developers/faucet + let client = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" + .parse::() + .unwrap() + .connect(provider); + + let factory = ContractFactory::new(abi, bytecode, &client); + let deployer = factory.deploy("initial value".to_string()).unwrap(); + let contract = deployer.send().await.unwrap(); + + let value: String = contract + .method("getValue", ()) + .unwrap() + .call() + .await + .unwrap(); + assert_eq!(value, "initial value"); + + // make a state mutating transaction + let pending_tx = contract + .method::<_, H256>("setValue", "hi".to_owned()) + .unwrap() + .send() + .await + .unwrap(); + let _receipt = pending_tx.await.unwrap(); + + let value: String = contract + .method("getValue", ()) + .unwrap() + .call() + .await + .unwrap(); + assert_eq!(value, "hi"); + } } diff --git a/ethers-contract/tests/get_past_events.rs b/ethers-contract/tests/get_past_events.rs deleted file mode 100644 index dcd51ed1..00000000 --- a/ethers-contract/tests/get_past_events.rs +++ /dev/null @@ -1,32 +0,0 @@ -use ethers_core::types::H256; - -mod common; -use common::{compile, connect, deploy, ValueChanged}; - -#[tokio::test] -async fn get_past_events() { - let (abi, bytecode) = compile(); - let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let (_ganache, contract) = deploy(&client, abi, bytecode).await; - - // make a call with `client2` - let _tx_hash = contract - .method::<_, H256>("setValue", "hi".to_owned()) - .unwrap() - .send() - .await - .unwrap(); - - // and we can fetch the events - let logs: Vec = contract - .event("ValueChanged") - .unwrap() - .from_block(0u64) - .topic1(client.address()) // Corresponds to the first indexed parameter - .query() - .await - .unwrap(); - assert_eq!(logs[0].new_value, "initial value"); - assert_eq!(logs[1].new_value, "hi"); - assert_eq!(logs.len(), 2); -} diff --git a/ethers-contract/tests/watch_events.rs b/ethers-contract/tests/watch_events.rs deleted file mode 100644 index d8b9ccaa..00000000 --- a/ethers-contract/tests/watch_events.rs +++ /dev/null @@ -1,38 +0,0 @@ -use ethers_core::types::H256; -use ethers_providers::StreamExt; - -mod common; -use common::{compile, connect, deploy, ValueChanged}; - -#[tokio::test] -async fn watch_events() { - let (abi, bytecode) = compile(); - let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let (_ganache, contract) = deploy(&client, abi, bytecode).await; - - // We spawn the event listener: - let mut stream = contract - .event::("ValueChanged") - .unwrap() - .stream() - .await - .unwrap(); - - let num_calls = 3u64; - - // and we make a few calls - for i in 0..num_calls { - let _tx_hash = contract - .method::<_, H256>("setValue", i.to_string()) - .unwrap() - .send() - .await - .unwrap(); - } - - for i in 0..num_calls { - // unwrap the option of the stream, then unwrap the decoding result - let log = stream.next().await.unwrap().unwrap(); - assert_eq!(log.new_value, i.to_string()); - } -} diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 7bd656d9..1694a8a8 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -31,4 +31,5 @@ bincode = "1.2.1" [features] default = ["abi"] +celo = [] # celo support extends the transaction format with extra fields abi = ["ethabi", "arrayvec"] diff --git a/ethers-core/src/types/chainstate/transaction.rs b/ethers-core/src/types/chainstate/transaction.rs index ce7d990e..d88cf801 100644 --- a/ethers-core/src/types/chainstate/transaction.rs +++ b/ethers-core/src/types/chainstate/transaction.rs @@ -9,7 +9,11 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; // Number of tx fields before signing +#[cfg(not(feature = "celo"))] const UNSIGNED_TX_FIELDS: usize = 6; +// Celo has 3 additional fields +#[cfg(feature = "celo")] +const UNSIGNED_TX_FIELDS: usize = 9; // Unsigned fields + signature [r s v] const SIGNED_TX_FIELDS: usize = UNSIGNED_TX_FIELDS + 3; @@ -46,6 +50,22 @@ pub struct TransactionRequest { /// Transaction nonce (None for next available nonce) #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option, + + ///////////////// Celo-specific transaction fields ///////////////// + /// The currency fees are paid in (None for native currency) + #[cfg(feature = "celo")] + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_currency: Option
, + + /// Gateway fee recipient (None for no gateway fee paid) + #[cfg(feature = "celo")] + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway_fee_recipient: Option
, + + /// Gateway fee amount (None for no gateway fee paid) + #[cfg(feature = "celo")] + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway_fee: Option, } impl TransactionRequest { @@ -54,17 +74,12 @@ impl TransactionRequest { Self::default() } - /// Convenience function for sending a new payment transaction to the receiver. The - /// `gas`, `gas_price` and `nonce` fields are left empty, to be populated + /// Convenience function for sending a new payment transaction to the receiver. pub fn pay, V: Into>(to: T, value: V) -> Self { TransactionRequest { - from: None, to: Some(to.into()), - gas: None, - gas_price: None, value: Some(value.into()), - data: None, - nonce: None, + ..Default::default() } } @@ -150,9 +165,12 @@ impl TransactionRequest { /// Produces the RLP encoding of the transaction with the provided signature pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut rlp = RlpStream::new(); + + // construct the RLP body rlp.begin_list(SIGNED_TX_FIELDS); self.rlp_base(&mut rlp); + // append the signature rlp.append(&signature.v); rlp.append(&signature.r); rlp.append(&signature.s); @@ -164,12 +182,45 @@ impl TransactionRequest { rlp_opt(rlp, self.nonce); rlp_opt(rlp, self.gas_price); rlp_opt(rlp, self.gas); + + #[cfg(feature = "celo")] + self.inject_celo_metadata(rlp); + rlp_opt(rlp, self.to.as_ref()); rlp_opt(rlp, self.value); rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..])); } } +// Separate impl block for the celo-specific fields +#[cfg(feature = "celo")] +impl TransactionRequest { + // modifies the RLP stream with the Celo-specific information + fn inject_celo_metadata(&self, rlp: &mut RlpStream) { + rlp_opt(rlp, self.fee_currency); + rlp_opt(rlp, self.gateway_fee_recipient); + rlp_opt(rlp, self.gateway_fee); + } + + /// Sets the `fee_currency` field in the transaction to the provided value + pub fn fee_currency>(mut self, fee_currency: T) -> Self { + self.fee_currency = Some(fee_currency.into()); + self + } + + /// Sets the `gateway_fee` field in the transaction to the provided value + pub fn gateway_fee>(mut self, gateway_fee: T) -> Self { + self.gateway_fee = Some(gateway_fee.into()); + self + } + + /// Sets the `gateway_fee_recipient` field in the transaction to the provided value + pub fn gateway_fee_recipient>(mut self, gateway_fee_recipient: T) -> Self { + self.gateway_fee_recipient = Some(gateway_fee_recipient.into()); + self + } +} + fn rlp_opt(rlp: &mut RlpStream, opt: Option) { if let Some(ref inner) = opt { rlp.append(inner); @@ -230,9 +281,38 @@ pub struct Transaction { /// ECDSA signature s pub s: U256, + + ///////////////// Celo-specific transaction fields ///////////////// + /// The currency fees are paid in (None for native currency) + #[cfg(feature = "celo")] + #[serde(skip_serializing_if = "Option::is_none", rename = "feeCurrency")] + pub fee_currency: Option
, + + /// Gateway fee recipient (None for no gateway fee paid) + #[cfg(feature = "celo")] + #[serde( + skip_serializing_if = "Option::is_none", + rename = "gatewayFeeRecipient" + )] + pub gateway_fee_recipient: Option
, + + /// Gateway fee amount (None for no gateway fee paid) + #[cfg(feature = "celo")] + #[serde(skip_serializing_if = "Option::is_none", rename = "gatewayFee")] + pub gateway_fee: Option, } impl Transaction { + // modifies the RLP stream with the Celo-specific information + // This is duplicated from TransactionRequest. Is there a good way to get rid + // of this code duplication? + #[cfg(feature = "celo")] + fn inject_celo_metadata(&self, rlp: &mut RlpStream) { + rlp_opt(rlp, self.fee_currency); + rlp_opt(rlp, self.gateway_fee_recipient); + rlp_opt(rlp, self.gateway_fee); + } + pub fn hash(&self) -> H256 { keccak256(&self.rlp().0).into() } @@ -243,6 +323,10 @@ impl Transaction { rlp.append(&self.nonce); rlp.append(&self.gas_price); rlp.append(&self.gas); + + #[cfg(feature = "celo")] + self.inject_celo_metadata(&mut rlp); + rlp_opt(&mut rlp, self.to); rlp.append(&self.value); rlp.append(&self.input.0); @@ -292,6 +376,7 @@ pub struct TransactionReceipt { } #[cfg(test)] +#[cfg(not(feature = "celo"))] mod tests { use super::*; diff --git a/ethers-core/src/types/crypto/keys.rs b/ethers-core/src/types/crypto/keys.rs index 1e66b9ad..f38e7eb9 100644 --- a/ethers-core/src/types/crypto/keys.rs +++ b/ethers-core/src/types/crypto/keys.rs @@ -157,6 +157,14 @@ impl PrivateKey { block_hash: None, block_number: None, transaction_index: None, + + // Celo support + #[cfg(feature = "celo")] + fee_currency: tx.fee_currency, + #[cfg(feature = "celo")] + gateway_fee: tx.gateway_fee, + #[cfg(feature = "celo")] + gateway_fee_recipient: tx.gateway_fee_recipient, }) } @@ -318,9 +326,9 @@ mod tests { } #[test] + #[cfg(not(feature = "celo"))] fn signs_tx() { use crate::types::{Address, Bytes}; - // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction let tx = TransactionRequest { diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index ea102572..78e4119c 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -25,3 +25,6 @@ ethers = { version = "0.1.0", path = "../ethers" } rustc-hex = "2.1.0" tokio = { version = "0.2.21", default-features = false, features = ["rt-core", "macros"] } + +[features] +celo = ["ethers-core/celo"] diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs index 9fcf11fc..aa48b3ab 100644 --- a/ethers-providers/tests/provider.rs +++ b/ethers-providers/tests/provider.rs @@ -1,12 +1,14 @@ -use ethers::{ - providers::{Http, Provider}, - types::TransactionRequest, - utils::{parse_ether, Ganache}, -}; +use ethers::providers::{Http, Provider}; use std::convert::TryFrom; #[tokio::test] +#[cfg(not(feature = "celo"))] async fn pending_txs_with_confirmations_ganache() { + use ethers::{ + types::TransactionRequest, + utils::{parse_ether, Ganache}, + }; + let _ganache = Ganache::new().block_time(2u64).spawn(); let provider = Provider::::try_from("http://localhost:8545").unwrap(); let accounts = provider.get_accounts().await.unwrap(); @@ -19,3 +21,25 @@ async fn pending_txs_with_confirmations_ganache() { // got the correct receipt assert_eq!(receipt.transaction_hash, hash); } + +#[cfg(feature = "celo")] +mod celo_tests { + use super::*; + use ethers::types::H256; + + #[tokio::test] + // https://alfajores-blockscout.celo-testnet.org/tx/0x544ea96cddb16aeeaedaf90885c1e02be4905f3eb43d6db3f28cac4dbe76a625/internal_transactions + async fn get_transaction() { + let provider = + Provider::::try_from("https://alfajores-forno.celo-testnet.org").unwrap(); + + let tx_hash = "544ea96cddb16aeeaedaf90885c1e02be4905f3eb43d6db3f28cac4dbe76a625" + .parse::() + .unwrap(); + let tx = provider.get_transaction(tx_hash).await.unwrap(); + assert!(tx.gateway_fee_recipient.is_none()); + assert_eq!(tx.gateway_fee.unwrap(), 0.into()); + assert_eq!(tx.hash, tx_hash); + assert_eq!(tx.block_number.unwrap(), 1100845.into()) + } +} diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index b2dd29f0..c26f3147 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -15,3 +15,6 @@ serde = "1.0.112" ethers = { version = "0.1.0", path = "../ethers" } tokio = { version = "0.2.21", features = ["macros"] } + +[features] +celo = ["ethers-core/celo", "ethers-providers/celo"] diff --git a/ethers-signers/tests/send_eth.rs b/ethers-signers/tests/send_eth.rs deleted file mode 100644 index 7edfc62c..00000000 --- a/ethers-signers/tests/send_eth.rs +++ /dev/null @@ -1,66 +0,0 @@ -use ethers::{ - providers::{Http, Provider}, - signers::Wallet, - types::TransactionRequest, - utils::{parse_ether, Ganache}, -}; -use std::convert::TryFrom; - -#[tokio::test] -async fn pending_txs_with_confirmations_rinkeby_infura() { - let provider = - Provider::::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27") - .unwrap(); - - // pls do not drain this key :) - // note: this works even if there's no EIP-155 configured! - let client = "FF7F80C6E9941865266ED1F481263D780169F1D98269C51167D20C630A5FDC8A" - .parse::() - .unwrap() - .connect(provider); - - let tx = TransactionRequest::pay(client.address(), parse_ether(1u64).unwrap()); - let pending_tx = client.send_transaction(tx, None).await.unwrap(); - let hash = *pending_tx; - dbg!(hash); - let receipt = pending_tx.confirmations(3).await.unwrap(); - - // got the correct receipt - assert_eq!(receipt.transaction_hash, hash); -} - -#[tokio::test] -async fn send_eth() { - let port = 8545u64; - let url = format!("http://localhost:{}", port).to_string(); - let _ganache = Ganache::new() - .port(port) - .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") - .spawn(); - - // this private key belongs to the above mnemonic - let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" - .parse() - .unwrap(); - - // connect to the network - let provider = Provider::::try_from(url.as_str()).unwrap(); - - // connect the wallet to the provider - let client = wallet.connect(provider); - - // craft the transaction - let tx = TransactionRequest::new() - .send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24") - .unwrap() - .value(10000); - - let balance_before = client.get_balance(client.address(), None).await.unwrap(); - - // send it! - client.send_transaction(tx, None).await.unwrap(); - - let balance_after = client.get_balance(client.address(), None).await.unwrap(); - - assert!(balance_before > balance_after); -} diff --git a/ethers-signers/tests/signer.rs b/ethers-signers/tests/signer.rs new file mode 100644 index 00000000..62cb85b0 --- /dev/null +++ b/ethers-signers/tests/signer.rs @@ -0,0 +1,98 @@ +use ethers::{ + providers::{Http, Provider}, + signers::Wallet, + types::TransactionRequest, +}; +use std::convert::TryFrom; + +#[cfg(not(feature = "celo"))] +mod eth_tests { + use super::*; + use ethers::utils::{parse_ether, Ganache}; + + #[tokio::test] + async fn pending_txs_with_confirmations_rinkeby_infura() { + let provider = Provider::::try_from( + "https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", + ) + .unwrap(); + + // pls do not drain this key :) + // note: this works even if there's no EIP-155 configured! + let client = "FF7F80C6E9941865266ED1F481263D780169F1D98269C51167D20C630A5FDC8A" + .parse::() + .unwrap() + .connect(provider); + + let tx = TransactionRequest::pay(client.address(), parse_ether(1u64).unwrap()); + let pending_tx = client.send_transaction(tx, None).await.unwrap(); + let hash = *pending_tx; + dbg!(hash); + let receipt = pending_tx.confirmations(3).await.unwrap(); + + // got the correct receipt + assert_eq!(receipt.transaction_hash, hash); + } + + #[tokio::test] + async fn send_eth() { + let port = 8545u64; + let url = format!("http://localhost:{}", port).to_string(); + let _ganache = Ganache::new() + .port(port) + .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") + .spawn(); + + // this private key belongs to the above mnemonic + let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" + .parse() + .unwrap(); + + // connect to the network + let provider = Provider::::try_from(url.as_str()).unwrap(); + + // connect the wallet to the provider + let client = wallet.connect(provider); + + // craft the transaction + let tx = TransactionRequest::new() + .send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24") + .unwrap() + .value(10000); + + let balance_before = client.get_balance(client.address(), None).await.unwrap(); + + // send it! + client.send_transaction(tx, None).await.unwrap(); + + let balance_after = client.get_balance(client.address(), None).await.unwrap(); + + assert!(balance_before > balance_after); + } +} + +#[cfg(feature = "celo")] +mod celo_tests { + use super::*; + + #[tokio::test] + async fn test_send_transaction() { + // Celo testnet + let provider = + Provider::::try_from("https://alfajores-forno.celo-testnet.org").unwrap(); + + // Funded with https://celo.org/developers/faucet + // Please do not drain this account :) + let client = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" + .parse::() + .unwrap() + .connect(provider); + + let balance_before = client.get_balance(client.address(), None).await.unwrap(); + let tx = TransactionRequest::pay(client.address(), 100); + let pending_tx = client.send_transaction(tx, None).await.unwrap(); + let _receipt = pending_tx.confirmations(3).await.unwrap(); + let balance_after = client.get_balance(client.address(), None).await.unwrap(); + assert!(balance_before > balance_after); + } +} diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml index d2df300c..50d88731 100644 --- a/ethers/Cargo.toml +++ b/ethers/Cargo.toml @@ -28,6 +28,13 @@ full = [ "core", ] +celo = [ + "ethers-core/celo", + "ethers-providers/celo", + "ethers-signers/celo", + "ethers-contract/celo", +] + core = ["ethers-core"] contract = ["ethers-contract"] providers = ["ethers-providers"]