docs(contract): expand contract docs

This commit is contained in:
Georgios Konstantopoulos 2020-06-10 21:20:47 +03:00
parent 1adbca67b0
commit 030fc671fe
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
8 changed files with 259 additions and 29 deletions

5
Cargo.lock generated
View File

@ -299,6 +299,7 @@ dependencies = [
"once_cell",
"rustc-hex",
"serde",
"serde_json",
"thiserror",
"tokio",
]
@ -1142,9 +1143,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.53"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2"
checksum = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226"
dependencies = [
"itoa",
"ryu",

View File

@ -20,6 +20,7 @@ tokio = { version = "0.2.21", default-features = false }
[dev-dependencies]
tokio = { version = "0.2.21", default-features = false, features = ["macros"] }
serde_json = "1.0.55"
[features]
default = ["abigen"]

View File

@ -10,32 +10,45 @@ use std::{fmt::Debug, marker::PhantomData};
use thiserror::Error as ThisError;
#[derive(ThisError, Debug)]
/// An Error which is thrown when interacting with a smart contract
pub enum ContractError {
/// Thrown when the ABI decoding fails
#[error(transparent)]
DecodingError(#[from] AbiError),
/// Thrown when detokenizing an argument
#[error(transparent)]
DetokenizationError(#[from] InvalidOutputType),
/// Thrown when a client call fails
#[error(transparent)]
ClientError(#[from] ClientError),
/// Thrown when a provider call fails
#[error(transparent)]
ProviderError(#[from] ProviderError),
/// Thrown during deployment if a constructor argument was passed in the `deploy`
/// call but a constructor was not present in the ABI
#[error("constructor is not defined in the ABI")]
ConstructorError,
/// Thrown if a contract address is not found in the deployment transaction's
/// receipt
#[error("Contract was not deployed")]
ContractNotDeployed,
}
#[derive(Debug, Clone)]
/// Helper for managing a transaction before submitting it to a node
pub struct ContractCall<'a, P, S, D> {
pub(crate) tx: TransactionRequest,
pub(crate) function: Function,
/// The raw transaction object
pub tx: TransactionRequest,
/// The ABI of the function being called
pub function: Function,
/// Optional block number to be used when calculating the transaction's gas and nonce
pub block: Option<BlockNumber>,
pub(crate) client: &'a Client<P, S>,
pub(crate) block: Option<BlockNumber>,
pub(crate) datatype: PhantomData<D>,
}

View File

@ -1,4 +1,4 @@
use crate::{ContractCall, Event};
use super::{call::ContractCall, event::Event};
use ethers_core::{
abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize},
@ -10,10 +10,153 @@ use ethers_signers::{Client, Signer};
use rustc_hex::ToHex;
use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData};
/// Represents a contract instance at an address. Provides methods for
/// contract interaction.
// TODO: Should we separate the lifetimes for the two references?
// https://stackoverflow.com/a/29862184
/// A Contract is an abstraction of an executable program on the Ethereum Blockchain.
/// It has code (called byte code) as well as allocated long-term memory
/// (called storage). Every deployed Contract has an address, which is used to connect
/// to it so that it may be sent messages to call its methods.
///
/// A Contract can emit Events, which can be efficiently observed by applications
/// to be notified when a contract has performed specific operation.
///
/// There are two types of methods that can be called on a Contract:
///
/// 1. A Constant method may not add, remove or change any data in the storage,
/// nor log any events, and may only call Constant methods on other contracts.
/// These methods are free (no Ether is required) to call. The result from them
/// may also be returned to the caller. Constant methods are marked as `pure` and
/// `view` in Solidity.
///
/// 2. A Non-Constant method requires a fee (in Ether) to be paid, but may perform
/// any state-changing operation desired, log events, send ether and call Non-Constant
/// methods on other Contracts. These methods cannot return their result to the caller.
/// These methods must be triggered by a transaction, sent by an Externally Owned Account
/// (EOA) either directly or indirectly (i.e. called from another contract), and are
/// required to be mined before the effects are present. Therefore, the duration
/// required for these operations can vary widely, and depend on the transaction
/// gas price, network congestion and miner priority heuristics.
///
/// The Contract API provides simple way to connect to a Contract and call its methods,
/// as functions on a Rust struct, handling all the binary protocol conversion,
/// internal name mangling and topic construction. This allows a Contract object
/// to be used like any standard Rust struct, without having to worry about the
/// low-level details of the Ethereum Virtual Machine or Blockchain.
///
/// The Contract definition (called an Application Binary Interface, or ABI) must
/// be provided to instantiate a contract and the available methods and events will
/// be made available to call by providing their name as a `str` via the [`method`]
/// and [`event`] methods. If non-existing names are given, the function/event call
/// will fail.
///
/// Alternatively, you can _and should_ use the [`abigen`] macro, or the [`Abigen` builder]
/// to generate type-safe bindings to your contracts.
///
/// # Example
///
/// Assuming we already have our contract deployed at `address`, we'll proceed to
/// interact with its methods and retrieve raw logs it has emitted.
///
/// ```no_run
/// use ethers_core::{abi::Abi, utils::Solc, types::{Address, H256}};
/// use ethers_contract::Contract;
/// use ethers_providers::{Provider, Http};
/// use ethers_signers::Wallet;
/// use std::convert::TryFrom;
///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// // this is a fake address used just for this example
/// let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
///
/// // (ugly way to write the ABI inline, you can otherwise read it from a file)
/// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?;
///
/// // connect to the network
/// let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// .parse::<Wallet>()?.connect(provider);
///
/// // create the contract object at the address
/// let contract = Contract::new(address, &abi, &client);
///
/// // Calling constant methods is done by calling `call()` on the method builder.
/// // (if the function takes no arguments, then you must use `()` as the argument)
/// let init_value: String = contract
/// .method::<_, String>("getValue", ())?
/// .call()
/// .await?;
///
/// // Non-constant methods are executed via the `send()` call on the method builder.
/// let tx_hash = contract
/// .method::<_, H256>("setValue", "hi".to_owned())?
/// .send()
/// .await?;
///
/// # Ok(())
/// # }
/// ```
///
/// # Event Logging
/// Querying structured logs requires you to have defined a struct with the expected
/// datatypes and to have implemented `Detokenize` for it. This boilerplate code
/// is generated for you via the [`abigen`] and [`Abigen` builder] utilities.
///
/// ```no_run
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// use ethers_core::{abi::Abi, types::Address};
/// use ethers_contract::Contract;
/// use ethers_providers::{Provider, Http};
/// use ethers_signers::Wallet;
/// use std::convert::TryFrom;
/// use ethers_core::abi::{Detokenize, Token, InvalidOutputType};
/// # // this is a fake address used just for this example
/// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
/// # let abi: Abi = serde_json::from_str(r#"[]"#)?;
/// # let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::<Wallet>()?.connect(provider);
/// # let contract = Contract::new(address, &abi, &client);
///
/// #[derive(Clone, Debug)]
/// struct ValueChanged {
/// old_author: Address,
/// new_author: Address,
/// old_value: String,
/// new_value: String,
/// }
///
/// impl Detokenize for ValueChanged {
/// fn from_tokens(tokens: Vec<Token>) -> Result<ValueChanged, InvalidOutputType> {
/// let old_author: Address = tokens[1].clone().to_address().unwrap();
/// let new_author: Address = tokens[1].clone().to_address().unwrap();
/// let old_value = tokens[2].clone().to_string().unwrap();
/// let new_value = tokens[3].clone().to_string().unwrap();
///
/// Ok(Self {
/// old_author,
/// new_author,
/// old_value,
/// new_value,
/// })
/// }
/// }
///
///
/// let logs: Vec<ValueChanged> = contract
/// .event("ValueChanged")?
/// .from_block(0u64)
/// .query()
/// .await?;
///
/// println!("{:?}", logs);
/// # Ok(())
/// # }
///
/// ```
///
/// _Disclaimer: these above docs have been adapted from the corresponding [ethers.js page](https://docs.ethers.io/ethers.js/html/api-contract.html)_
///
/// [`abigen`]: macro.abigen.html
/// [`Abigen` builder]: struct.Abigen.html
/// [`event`]: struct.Contract.html#method.event
/// [`method`]: struct.Contract.html#method.method
#[derive(Debug, Clone)]
pub struct Contract<'a, P, S> {
client: &'a Client<P, S>,

View File

@ -9,42 +9,51 @@ use ethers_core::{
use std::{collections::HashMap, marker::PhantomData};
/// Helper for managing the event filter before querying or streaming its logs
pub struct Event<'a: 'b, 'b, P, D> {
/// The event filter's state
pub filter: Filter,
/// The ABI of the event which is being filtered
pub event: &'b AbiEvent,
pub(crate) provider: &'a Provider<P>,
pub(crate) event: &'b AbiEvent,
pub(crate) datatype: PhantomData<D>,
}
// TODO: Improve these functions
impl<P, D: Detokenize> Event<'_, '_, P, D> {
/// Sets the filter's `from` block
#[allow(clippy::wrong_self_convention)]
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.from_block = Some(block.into());
self
}
/// Sets the filter's `to` block
#[allow(clippy::wrong_self_convention)]
pub fn to_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.to_block = Some(block.into());
self
}
/// Sets the filter's 0th topic (typically the event name for non-anonymous events)
pub fn topic0<T: Into<ValueOrArray<H256>>>(mut self, topic: T) -> Self {
self.filter.topics[0] = Some(topic.into());
self
}
/// Sets the filter's 1st topic
pub fn topic1<T: Into<ValueOrArray<H256>>>(mut self, topic: T) -> Self {
self.filter.topics[1] = Some(topic.into());
self
}
/// Sets the filter's 2nd topic
pub fn topic2<T: Into<ValueOrArray<H256>>>(mut self, topic: T) -> Self {
self.filter.topics[2] = Some(topic.into());
self
}
/// Sets the filter's 3rd topic
pub fn topic3<T: Into<ValueOrArray<H256>>>(mut self, topic: T) -> Self {
self.filter.topics[3] = Some(topic.into());
self

View File

@ -11,13 +11,14 @@ use std::time::Duration;
use tokio::time;
/// Poll for tx confirmation once every 7 seconds.
/// TODO: Can this be improved by replacing polling with an "on new block" subscription?
// TODO: Can this be improved by replacing polling with an "on new block" subscription?
const POLL_INTERVAL: u64 = 7000;
#[derive(Debug, Clone)]
/// Helper which manages the deployment transaction of a smart contract
pub struct Deployer<'a, P, S> {
abi: &'a Abi,
client: &'a Client<P, S>,
pub abi: &'a Abi,
tx: TransactionRequest,
confs: usize,
poll_interval: Duration,
@ -28,16 +29,22 @@ where
S: Signer,
P: JsonRpcClient,
{
/// Sets the poll frequency for checking the number of confirmations for
/// the contract deployment transaction
pub fn poll_interval<T: Into<Duration>>(mut self, interval: T) -> Self {
self.poll_interval = interval.into();
self
}
/// Sets the number of confirmations to wait for the contract deployment transaction
pub fn confirmations<T: Into<usize>>(mut self, confirmations: T) -> Self {
self.confs = confirmations.into();
self
}
/// Broadcasts the contract deployment transaction and after waiting for it to
/// be sufficiently confirmed (default: 1), it returns a [`Contract`](./struct.Contract.html)
/// struct at the deployed contract's address.
pub async fn send(self) -> Result<Contract<'a, P, S>, ContractError> {
let tx_hash = self.client.send_transaction(self.tx, None).await?;
@ -58,16 +65,60 @@ where
Ok(contract)
}
/// Returns a reference to the deployer's ABI
pub fn abi(&self) -> &Abi {
&self.abi
}
/// Returns a reference to the deployer's client
pub fn client(&self) -> &Client<P, S> {
&self.client
}
}
#[derive(Debug, Clone)]
/// To deploy a contract to the Ethereum network, a `ContractFactory` can be
/// created which manages the Contract bytecode and Application Binary Interface
/// (ABI), usually generated from the Solidity compiler.
///
/// Once the factory's deployment transaction is mined with sufficient confirmations,
/// the [`Contract`](./struct.Contract.html) object is returned.
///
/// # Example
///
/// ```no_run
/// use ethers_core::utils::Solc;
/// use ethers_contract::ContractFactory;
/// use ethers_providers::{Provider, Http};
/// use ethers_signers::Wallet;
/// use std::convert::TryFrom;
///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// // first we'll compile the contract (you can alternatively compile it yourself
/// // and pass the ABI/Bytecode
/// let compiled = Solc::new("./tests/contract.sol").build().unwrap();
/// let contract = compiled
/// .get("SimpleStorage")
/// .expect("could not find contract");
///
/// // connect to the network
/// let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// .parse::<Wallet>()?.connect(provider);
///
/// // create a factory which will be used to deploy instances of the contract
/// let factory = ContractFactory::new(&contract.abi, &contract.bytecode, &client);
///
/// // The deployer created by the `deploy` call exposes a builder which gets consumed
/// // by the async `send` call
/// let contract = factory
/// .deploy("initial value".to_string())?
/// .confirmations(0usize)
/// .send()
/// .await?;
/// println!("{}", contract.address());
/// # Ok(())
/// # }
pub struct ContractFactory<'a, P, S> {
client: &'a Client<P, S>,
abi: &'a Abi,
@ -79,8 +130,10 @@ where
S: Signer,
P: JsonRpcClient,
{
/// Instantiate a new contract factory
pub fn new(client: &'a Client<P, S>, abi: &'a Abi, bytecode: &'a Bytes) -> Self {
/// Creates a factory for deployment of the Contract with bytecode, and the
/// constructor defined in the abi. The client will be used to send any deployment
/// transaction.
pub fn new(abi: &'a Abi, bytecode: &'a Bytes, client: &'a Client<P, S>) -> Self {
Self {
client,
abi,
@ -88,8 +141,14 @@ where
}
}
/// Deploys an instance of the contract with the provider constructor arguments
/// and returns the contract's instance
/// Constructs the deployment transaction based on the provided constructor
/// arguments and returns a `Deployer` instance. You must call `send()` in order
/// to actually deploy the contract.
///
/// Notes:
/// 1. If there are no constructor arguments, you should pass `()` as the argument.
/// 1. The default poll duration is 7 seconds.
/// 1. The default number of confirmations is 1 block.
pub fn deploy<T: Tokenize>(
&self,
constructor_args: T,

View File

@ -1,24 +1,28 @@
mod contract;
pub use contract::Contract;
mod event;
pub use event::Event;
mod call;
pub use call::{ContractCall, ContractError};
pub use call::ContractError;
mod factory;
pub use factory::ContractFactory;
mod event;
/// This module exposes low lever builder structures which are only consumed by the
/// type-safe ABI bindings generators.
pub mod builders {
pub use super::call::ContractCall;
pub use super::event::Event;
pub use super::factory::Deployer;
}
#[cfg(feature = "abigen")]
pub use ethers_contract_abigen::Abigen;
#[cfg(feature = "abigen")]
pub use ethers_contract_derive::abigen;
// re-export for convenience
pub use ethers_core::abi;
pub use ethers_core::types;
pub use ethers_providers as providers;
pub use ethers_signers as signers;
// Hide the Lazy re-export, it's just for convenience
#[doc(hidden)]
pub use once_cell::sync::Lazy;

View File

@ -2,7 +2,7 @@ use ethers_contract::ContractFactory;
use ethers_core::{
abi::{Detokenize, InvalidOutputType, Token},
types::{Address, H256},
utils::{GanacheBuilder, Solc},
utils::{Ganache, Solc},
};
use ethers_providers::{Http, Provider};
use ethers_signers::Wallet;
@ -19,7 +19,7 @@ async fn deploy_and_call_contract() {
// launch ganache
let port = 8546u64;
let url = format!("http://localhost:{}", port).to_string();
let _ganache = GanacheBuilder::new().port(port)
let _ganache = Ganache::new().port(port)
.mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
.spawn();
@ -42,7 +42,7 @@ async fn deploy_and_call_contract() {
let client2 = wallet2.connect(provider);
// create a factory which will be used to deploy instances of the contract
let factory = ContractFactory::new(&client, &contract.abi, &contract.bytecode);
let factory = ContractFactory::new(&contract.abi, &contract.bytecode, &client);
// `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