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
This commit is contained in:
parent
20493e0190
commit
ba5ae5a894
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
21
README.md
21
README.md
|
@ -1,6 +1,6 @@
|
|||
# <h1 align="center"> ethers.rs </h1>
|
||||
|
||||
**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" }
|
|||
|
||||
</details>
|
||||
|
||||
### 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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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<P, S>,
|
||||
tx: TransactionRequest,
|
||||
confs: usize,
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ValueChanged> = 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>("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::<Http>::try_from("https://alfajores-forno.celo-testnet.org").unwrap();
|
||||
|
||||
// Funded with https://celo.org/developers/faucet
|
||||
let client = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1"
|
||||
.parse::<Wallet>()
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ValueChanged> = 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);
|
||||
}
|
|
@ -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>("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());
|
||||
}
|
||||
}
|
|
@ -31,4 +31,5 @@ bincode = "1.2.1"
|
|||
|
||||
[features]
|
||||
default = ["abi"]
|
||||
celo = [] # celo support extends the transaction format with extra fields
|
||||
abi = ["ethabi", "arrayvec"]
|
||||
|
|
|
@ -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<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")]
|
||||
pub fee_currency: Option<Address>,
|
||||
|
||||
/// Gateway fee recipient (None for no gateway fee paid)
|
||||
#[cfg(feature = "celo")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gateway_fee_recipient: Option<Address>,
|
||||
|
||||
/// Gateway fee amount (None for no gateway fee paid)
|
||||
#[cfg(feature = "celo")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gateway_fee: Option<U256>,
|
||||
}
|
||||
|
||||
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<T: Into<NameOrAddress>, V: Into<U256>>(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<T: Into<Address>>(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<T: Into<U256>>(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<T: Into<Address>>(mut self, gateway_fee_recipient: T) -> Self {
|
||||
self.gateway_fee_recipient = Some(gateway_fee_recipient.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn rlp_opt<T: rlp::Encodable>(rlp: &mut RlpStream, opt: Option<T>) {
|
||||
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<Address>,
|
||||
|
||||
/// 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<Address>,
|
||||
|
||||
/// 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<U256>,
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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::<Http>::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::<Http>::try_from("https://alfajores-forno.celo-testnet.org").unwrap();
|
||||
|
||||
let tx_hash = "544ea96cddb16aeeaedaf90885c1e02be4905f3eb43d6db3f28cac4dbe76a625"
|
||||
.parse::<H256>()
|
||||
.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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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::<Http>::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::<Wallet>()
|
||||
.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::<Http>::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);
|
||||
}
|
|
@ -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::<Http>::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::<Wallet>()
|
||||
.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::<Http>::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::<Http>::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::<Wallet>()
|
||||
.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);
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
|
|
Loading…
Reference in New Issue