From 030fc671fee0a7035632a25f411f9691d89afe4b Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 10 Jun 2020 21:20:47 +0300 Subject: [PATCH] docs(contract): expand contract docs --- Cargo.lock | 5 +- ethers-contract/Cargo.toml | 1 + ethers-contract/src/call.rs | 19 +++- ethers-contract/src/contract.rs | 153 +++++++++++++++++++++++++++++- ethers-contract/src/event.rs | 11 ++- ethers-contract/src/factory.rs | 71 ++++++++++++-- ethers-contract/src/lib.rs | 22 +++-- ethers-contract/tests/contract.rs | 6 +- 8 files changed, 259 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3703b239..2ff32515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 5a9cd711..f7cf2096 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -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"] diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 28da6ae1..8c6d0b43 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -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, pub(crate) client: &'a Client, - pub(crate) block: Option, pub(crate) datatype: PhantomData, } diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index 05250120..a7ff34f7 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -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> { +/// // this is a fake address used just for this example +/// let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; +/// +/// // (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::::try_from("http://localhost:8545").unwrap(); +/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" +/// .parse::()?.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> { +/// 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::
()?; +/// # let abi: Abi = serde_json::from_str(r#"[]"#)?; +/// # let provider = Provider::::try_from("http://localhost:8545").unwrap(); +/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::()?.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) -> Result { +/// 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 = 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, diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 4c28a6b6..d13bbee6 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -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

, - pub(crate) event: &'b AbiEvent, pub(crate) datatype: PhantomData, } // TODO: Improve these functions impl Event<'_, '_, P, D> { + /// Sets the filter's `from` block #[allow(clippy::wrong_self_convention)] pub fn from_block>(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>(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>>(mut self, topic: T) -> Self { self.filter.topics[0] = Some(topic.into()); self } + /// Sets the filter's 1st topic pub fn topic1>>(mut self, topic: T) -> Self { self.filter.topics[1] = Some(topic.into()); self } + /// Sets the filter's 2nd topic pub fn topic2>>(mut self, topic: T) -> Self { self.filter.topics[2] = Some(topic.into()); self } + /// Sets the filter's 3rd topic pub fn topic3>>(mut self, topic: T) -> Self { self.filter.topics[3] = Some(topic.into()); self diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index 5833f202..24667373 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -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, - 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>(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>(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, 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 { &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> { +/// // 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::::try_from("http://localhost:8545").unwrap(); +/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" +/// .parse::()?.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, abi: &'a Abi, @@ -79,8 +130,10 @@ where S: Signer, P: JsonRpcClient, { - /// Instantiate a new contract factory - pub fn new(client: &'a Client, 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) -> 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( &self, constructor_args: T, diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 989a5576..7dbaa12a 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -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; diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index df4bddbd..05c40188 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -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