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"]