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
|
export PATH=$HOME/bin:$PATH
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
|
- name: cargo test (Celo)
|
||||||
|
run: |
|
||||||
|
export PATH=$HOME/bin:$PATH
|
||||||
|
cargo test --all-features
|
||||||
|
|
||||||
- name: cargo fmt
|
- name: cargo fmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
|
|
@ -155,6 +155,15 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cloudabi"
|
||||||
|
version = "0.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -290,6 +299,7 @@ dependencies = [
|
||||||
name = "ethers-contract"
|
name = "ethers-contract"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ethers",
|
||||||
"ethers-contract-abigen",
|
"ethers-contract-abigen",
|
||||||
"ethers-contract-derive",
|
"ethers-contract-derive",
|
||||||
"ethers-core",
|
"ethers-core",
|
||||||
|
@ -300,6 +310,7 @@ dependencies = [
|
||||||
"rustc-hex",
|
"rustc-hex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serial_test",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
@ -774,6 +785,15 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
|
@ -896,6 +916,30 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
@ -1155,6 +1199,12 @@ dependencies = [
|
||||||
"winapi 0.3.8",
|
"winapi 0.3.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -1208,6 +1258,28 @@ dependencies = [
|
||||||
"url",
|
"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]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
21
README.md
21
README.md
|
@ -1,6 +1,6 @@
|
||||||
# <h1 align="center"> ethers.rs </h1>
|
# <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)
|
[![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>
|
</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
|
## Features
|
||||||
|
|
||||||
- [x] Ethereum JSON-RPC Client
|
- [x] Ethereum JSON-RPC Client
|
||||||
|
@ -28,6 +46,7 @@ ethers = { git = "github.com/gakonst/ethers-rs" }
|
||||||
- [x] Querying past events
|
- [x] Querying past events
|
||||||
- [x] Event monitoring as `Stream`s
|
- [x] Event monitoring as `Stream`s
|
||||||
- [x] ENS as a first class citizen
|
- [x] ENS as a first class citizen
|
||||||
|
- [x] Celo support
|
||||||
- [ ] Websockets / `eth_subscribe`
|
- [ ] Websockets / `eth_subscribe`
|
||||||
- [ ] Hardware Wallet Support
|
- [ ] Hardware Wallet Support
|
||||||
- [ ] WASM Bindings
|
- [ ] WASM Bindings
|
||||||
|
|
|
@ -19,8 +19,11 @@ once_cell = { version = "1.3.1", default-features = false }
|
||||||
futures = "0.3.5"
|
futures = "0.3.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
ethers = { version = "0.1.0", path = "../ethers" }
|
||||||
tokio = { version = "0.2.21", default-features = false, features = ["macros"] }
|
tokio = { version = "0.2.21", default-features = false, features = ["macros"] }
|
||||||
serde_json = "1.0.55"
|
serde_json = "1.0.55"
|
||||||
|
serial_test = "0.4.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
abigen = ["ethers-contract-abigen", "ethers-contract-derive"]
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
/// Helper which manages the deployment transaction of a smart contract
|
/// Helper which manages the deployment transaction of a smart contract
|
||||||
pub struct Deployer<'a, P, S> {
|
pub struct Deployer<'a, P, S> {
|
||||||
|
/// The deployer's transaction, exposed for overriding the defaults
|
||||||
|
pub tx: TransactionRequest,
|
||||||
abi: Abi,
|
abi: Abi,
|
||||||
client: &'a Client<P, S>,
|
client: &'a Client<P, S>,
|
||||||
tx: TransactionRequest,
|
|
||||||
confs: usize,
|
confs: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
use ethers_contract::ContractFactory;
|
use ethers::{contract::ContractFactory, types::H256};
|
||||||
use ethers_core::{
|
|
||||||
types::{Address, H256},
|
|
||||||
utils::Ganache,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
|
mod eth_tests {
|
||||||
|
use super::*;
|
||||||
|
use ethers::{providers::StreamExt, types::Address, utils::Ganache};
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
async fn deploy_and_call_contract() {
|
async fn deploy_and_call_contract() {
|
||||||
let (abi, bytecode) = compile();
|
let (abi, bytecode) = compile();
|
||||||
|
|
||||||
|
@ -67,3 +70,121 @@ async fn deploy_and_call_contract() {
|
||||||
assert_eq!(init_address, Address::zero());
|
assert_eq!(init_address, Address::zero());
|
||||||
assert_eq!(init_value, "initial value");
|
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]
|
[features]
|
||||||
default = ["abi"]
|
default = ["abi"]
|
||||||
|
celo = [] # celo support extends the transaction format with extra fields
|
||||||
abi = ["ethabi", "arrayvec"]
|
abi = ["ethabi", "arrayvec"]
|
||||||
|
|
|
@ -9,7 +9,11 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
// Number of tx fields before signing
|
// Number of tx fields before signing
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
const UNSIGNED_TX_FIELDS: usize = 6;
|
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]
|
// Unsigned fields + signature [r s v]
|
||||||
const SIGNED_TX_FIELDS: usize = UNSIGNED_TX_FIELDS + 3;
|
const SIGNED_TX_FIELDS: usize = UNSIGNED_TX_FIELDS + 3;
|
||||||
|
@ -46,6 +50,22 @@ pub struct TransactionRequest {
|
||||||
/// Transaction nonce (None for next available nonce)
|
/// Transaction nonce (None for next available nonce)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub nonce: Option<U256>,
|
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 {
|
impl TransactionRequest {
|
||||||
|
@ -54,17 +74,12 @@ impl TransactionRequest {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function for sending a new payment transaction to the receiver. The
|
/// Convenience function for sending a new payment transaction to the receiver.
|
||||||
/// `gas`, `gas_price` and `nonce` fields are left empty, to be populated
|
|
||||||
pub fn pay<T: Into<NameOrAddress>, V: Into<U256>>(to: T, value: V) -> Self {
|
pub fn pay<T: Into<NameOrAddress>, V: Into<U256>>(to: T, value: V) -> Self {
|
||||||
TransactionRequest {
|
TransactionRequest {
|
||||||
from: None,
|
|
||||||
to: Some(to.into()),
|
to: Some(to.into()),
|
||||||
gas: None,
|
|
||||||
gas_price: None,
|
|
||||||
value: Some(value.into()),
|
value: Some(value.into()),
|
||||||
data: None,
|
..Default::default()
|
||||||
nonce: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,9 +165,12 @@ impl TransactionRequest {
|
||||||
/// Produces the RLP encoding of the transaction with the provided signature
|
/// Produces the RLP encoding of the transaction with the provided signature
|
||||||
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
|
|
||||||
|
// construct the RLP body
|
||||||
rlp.begin_list(SIGNED_TX_FIELDS);
|
rlp.begin_list(SIGNED_TX_FIELDS);
|
||||||
self.rlp_base(&mut rlp);
|
self.rlp_base(&mut rlp);
|
||||||
|
|
||||||
|
// append the signature
|
||||||
rlp.append(&signature.v);
|
rlp.append(&signature.v);
|
||||||
rlp.append(&signature.r);
|
rlp.append(&signature.r);
|
||||||
rlp.append(&signature.s);
|
rlp.append(&signature.s);
|
||||||
|
@ -164,12 +182,45 @@ impl TransactionRequest {
|
||||||
rlp_opt(rlp, self.nonce);
|
rlp_opt(rlp, self.nonce);
|
||||||
rlp_opt(rlp, self.gas_price);
|
rlp_opt(rlp, self.gas_price);
|
||||||
rlp_opt(rlp, self.gas);
|
rlp_opt(rlp, self.gas);
|
||||||
|
|
||||||
|
#[cfg(feature = "celo")]
|
||||||
|
self.inject_celo_metadata(rlp);
|
||||||
|
|
||||||
rlp_opt(rlp, self.to.as_ref());
|
rlp_opt(rlp, self.to.as_ref());
|
||||||
rlp_opt(rlp, self.value);
|
rlp_opt(rlp, self.value);
|
||||||
rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..]));
|
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>) {
|
fn rlp_opt<T: rlp::Encodable>(rlp: &mut RlpStream, opt: Option<T>) {
|
||||||
if let Some(ref inner) = opt {
|
if let Some(ref inner) = opt {
|
||||||
rlp.append(inner);
|
rlp.append(inner);
|
||||||
|
@ -230,9 +281,38 @@ pub struct Transaction {
|
||||||
|
|
||||||
/// ECDSA signature s
|
/// ECDSA signature s
|
||||||
pub s: U256,
|
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 {
|
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 {
|
pub fn hash(&self) -> H256 {
|
||||||
keccak256(&self.rlp().0).into()
|
keccak256(&self.rlp().0).into()
|
||||||
}
|
}
|
||||||
|
@ -243,6 +323,10 @@ impl Transaction {
|
||||||
rlp.append(&self.nonce);
|
rlp.append(&self.nonce);
|
||||||
rlp.append(&self.gas_price);
|
rlp.append(&self.gas_price);
|
||||||
rlp.append(&self.gas);
|
rlp.append(&self.gas);
|
||||||
|
|
||||||
|
#[cfg(feature = "celo")]
|
||||||
|
self.inject_celo_metadata(&mut rlp);
|
||||||
|
|
||||||
rlp_opt(&mut rlp, self.to);
|
rlp_opt(&mut rlp, self.to);
|
||||||
rlp.append(&self.value);
|
rlp.append(&self.value);
|
||||||
rlp.append(&self.input.0);
|
rlp.append(&self.input.0);
|
||||||
|
@ -292,6 +376,7 @@ pub struct TransactionReceipt {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,14 @@ impl PrivateKey {
|
||||||
block_hash: None,
|
block_hash: None,
|
||||||
block_number: None,
|
block_number: None,
|
||||||
transaction_index: 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]
|
#[test]
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
fn signs_tx() {
|
fn signs_tx() {
|
||||||
use crate::types::{Address, Bytes};
|
use crate::types::{Address, Bytes};
|
||||||
|
|
||||||
// retrieved test vector from:
|
// retrieved test vector from:
|
||||||
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
|
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
|
||||||
let tx = TransactionRequest {
|
let tx = TransactionRequest {
|
||||||
|
|
|
@ -25,3 +25,6 @@ ethers = { version = "0.1.0", path = "../ethers" }
|
||||||
|
|
||||||
rustc-hex = "2.1.0"
|
rustc-hex = "2.1.0"
|
||||||
tokio = { version = "0.2.21", default-features = false, features = ["rt-core", "macros"] }
|
tokio = { version = "0.2.21", default-features = false, features = ["rt-core", "macros"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
celo = ["ethers-core/celo"]
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use ethers::{
|
use ethers::providers::{Http, Provider};
|
||||||
providers::{Http, Provider},
|
|
||||||
types::TransactionRequest,
|
|
||||||
utils::{parse_ether, Ganache},
|
|
||||||
};
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
async fn pending_txs_with_confirmations_ganache() {
|
async fn pending_txs_with_confirmations_ganache() {
|
||||||
|
use ethers::{
|
||||||
|
types::TransactionRequest,
|
||||||
|
utils::{parse_ether, Ganache},
|
||||||
|
};
|
||||||
|
|
||||||
let _ganache = Ganache::new().block_time(2u64).spawn();
|
let _ganache = Ganache::new().block_time(2u64).spawn();
|
||||||
let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
|
let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
|
||||||
let accounts = provider.get_accounts().await.unwrap();
|
let accounts = provider.get_accounts().await.unwrap();
|
||||||
|
@ -19,3 +21,25 @@ async fn pending_txs_with_confirmations_ganache() {
|
||||||
// got the correct receipt
|
// got the correct receipt
|
||||||
assert_eq!(receipt.transaction_hash, hash);
|
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" }
|
ethers = { version = "0.1.0", path = "../ethers" }
|
||||||
|
|
||||||
tokio = { version = "0.2.21", features = ["macros"] }
|
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",
|
"core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
celo = [
|
||||||
|
"ethers-core/celo",
|
||||||
|
"ethers-providers/celo",
|
||||||
|
"ethers-signers/celo",
|
||||||
|
"ethers-contract/celo",
|
||||||
|
]
|
||||||
|
|
||||||
core = ["ethers-core"]
|
core = ["ethers-core"]
|
||||||
contract = ["ethers-contract"]
|
contract = ["ethers-contract"]
|
||||||
providers = ["ethers-providers"]
|
providers = ["ethers-providers"]
|
||||||
|
|
Loading…
Reference in New Issue