web3 0.3.0

Ethereum JSON-RPC client.
Documentation
//! Ethereum Contract Interface

use ethabi;

use std::time;
use api::{Eth, Namespace};
use confirm;
use contract::tokens::{Detokenize, Tokenize};
use types::{Address, Bytes, CallRequest, H256, TransactionRequest, TransactionCondition, U256, BlockNumber};
use {Transport};

mod error;
mod result;
pub mod deploy;
pub mod tokens;

pub use contract::result::{QueryResult, CallResult};
pub use contract::error::{Error, ErrorKind};

/// Contract Call/Query Options
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Options {
  /// Fixed gas limit
  pub gas: Option<U256>,
  /// Fixed gas price
  pub gas_price: Option<U256>,
  /// Value to transfer
  pub value: Option<U256>,
  /// Fixed transaction nonce
  pub nonce: Option<U256>,
  /// A conditon to satisfy before including transaction.
  pub condition: Option<TransactionCondition>,
}

impl Options {
  /// Create new default `Options` object with some modifications.
  pub fn with<F>(func: F) -> Options where
    F: FnOnce(&mut Options)
  {
      let mut options = Options::default();
      func(&mut options);
      options
  }
}

/// Ethereum Contract Interface
#[derive(Debug)]
pub struct Contract<T: Transport> {
  address: Address,
  eth: Eth<T>,
  abi: ethabi::Contract,
}

impl<T: Transport> Contract<T> {
  /// Creates deployment builder for a contract given it's ABI in JSON.
  pub fn deploy(eth: Eth<T>, json: &[u8]) -> Result<deploy::Builder<T>, ethabi::Error> {
    let abi = ethabi::Contract::load(json)?;
    Ok(deploy::Builder {
      eth,
      abi,
      options: Options::default(),
      confirmations: 1,
      poll_interval: time::Duration::from_secs(7),
    })
  }
}

impl<T: Transport> Contract<T> {
  /// Creates new Contract Interface given blockchain address and ABI
  pub fn new(eth: Eth<T>, address: Address, abi: ethabi::Contract) -> Self {
    Contract {
      address,
      eth,
      abi,
    }
  }

  /// Creates new Contract Interface given blockchain address and JSON containing ABI
  pub fn from_json(eth: Eth<T>, address: Address, json: &[u8]) -> Result<Self, ethabi::Error> {
    let abi = ethabi::Contract::load(json)?;
    Ok(Self::new(eth, address, abi))
  }

  /// Returns contract address
  pub fn address(&self) -> Address {
    self.address
  }

  /// Execute a contract function
  pub fn call<P>(&self, func: &str, params: P, from: Address, options: Options) -> CallResult<H256, T::Out> where
    P: Tokenize,
  {
    self.abi.function(func.into())
      .and_then(|function| function.encode_input(&params.into_tokens()))
      .map(move |data| {
        self.eth.send_transaction(TransactionRequest {
          from: from,
          to: Some(self.address.clone()),
          gas: options.gas,
          gas_price: options.gas_price,
          value: options.value,
          nonce: options.nonce,
          data: Some(Bytes(data)),
          condition: options.condition,
        }).into()
      })
      .unwrap_or_else(Into::into)
  }

  /// Execute a contract function and wait for confirmations
  pub fn call_with_confirmations<P>(&self, func: &str, params: P, from: Address, options: Options, confirmations: usize)
    -> confirm::SendTransactionWithConfirmation<T>
    where P: Tokenize
  {
    let poll_interval = time::Duration::from_secs(1);

    self.abi.function(func.into())
        .and_then(|function| function.encode_input(&params.into_tokens()))
        .map(|fn_data| {
          let transaction_request = TransactionRequest {
            from: from,
            to: Some(self.address.clone()),
            gas: options.gas,
            gas_price: options.gas_price,
            value: options.value,
            nonce: options.nonce,
            data: Some(Bytes(fn_data)),
            condition: options.condition,
          };

          confirm::send_transaction_with_confirmation(self.eth.transport().clone(), transaction_request, poll_interval, confirmations)
        })
        .unwrap_or_else(|e| {
          // TODO [ToDr] SendTransactionWithConfirmation should support custom error type (so that we can return
          // `contract::Error` instead of more generic `Error`.
          confirm::SendTransactionWithConfirmation::from_err(
            self.eth.transport().clone(),
            ::error::ErrorKind::Decoder(format!("{:?}", e))
          )
        })
  }

  /// Estimate gas required for this function call.
  pub fn estimate_gas<P>(&self, func: &str, params: P, from: Address, options: Options) -> CallResult<U256, T::Out> where
    P: Tokenize,
  {
    self.abi.function(func.into())
      .and_then(|function| function.encode_input(&params.into_tokens()))
      .map(|data| {
        self.eth.estimate_gas(CallRequest {
          from: Some(from),
          to: self.address.clone(),
          gas: options.gas,
          gas_price: options.gas_price,
          value: options.value,
          data: Some(Bytes(data)),
        }, None).into()
      })
      .unwrap_or_else(Into::into)
  }

  /// Call constant function
  pub fn query<R, A, B, P>(&self, func: &str, params: P, from: A, options: Options, block: B) -> QueryResult<R, T::Out> where
    R: Detokenize,
    A: Into<Option<Address>>,
    B: Into<Option<BlockNumber>>,
    P: Tokenize,
  {
    self.abi.function(func.into())
      .and_then(|function| function.encode_input(&params.into_tokens()).map(|call| (call, function)))
      .map(|(call, function)| {
        let result = self.eth.call(CallRequest {
          from: from.into(),
          to: self.address.clone(),
          gas: options.gas,
          gas_price: options.gas_price,
          value: options.value,
          data: Some(Bytes(call))
        }, block.into());
        QueryResult::new(result, function.clone())
      })
      .unwrap_or_else(Into::into)
  }
}

#[cfg(test)]
mod tests {
  use api::{self, Namespace};
  use futures::Future;
  use helpers::tests::TestTransport;
  use rpc;
  use types::{Address, H256, U256, BlockNumber};
  use {Transport};
  use super::{Contract, Options};

  fn contract<T: Transport>(transport: &T) -> Contract<&T> {
    let eth = api::Eth::new(transport);
    Contract::from_json(eth, 1.into(), include_bytes!("./res/token.json")).unwrap()
  }

  #[test]
  fn should_call_constant_function() {
    // given
    let mut transport = TestTransport::default();
    transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));

    let result: String = {
      let token = contract(&transport);

      // when
      token.query("name", (), None, Options::default(), BlockNumber::Number(1)).wait().unwrap()
    };

    // then
    transport.assert_request("eth_call", &[
      "{\"data\":\"0x06fdde03\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
      "\"0x1\"".into(),
    ]);
    transport.assert_no_more_requests();
    assert_eq!(result, "Hello World!".to_owned());
  }

  #[test]
  fn should_query_with_params() {
    // given
    let mut transport = TestTransport::default();
    transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));

    let result: String = {
      let token = contract(&transport);

      // when
      token.query("name", (), Address::from(5), Options::with(|options| {
        options.gas_price = Some(10_000_000.into());
      }), BlockNumber::Latest).wait().unwrap()
    };

    // then
    transport.assert_request("eth_call", &[
      "{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"gasPrice\":\"0x989680\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
      "\"latest\"".into(),
    ]);
    transport.assert_no_more_requests();
    assert_eq!(result, "Hello World!".to_owned());
  }

  #[test]
  fn should_call_a_contract_function() {
    // given
    let mut transport = TestTransport::default();
    transport.set_response(rpc::Value::String(format!("{:?}", H256::from(5))));

    let result = {
      let token = contract(&transport);

      // when
      token.call("name", (), 5.into(), Options::default()).wait().unwrap()
    };

    // then
    transport.assert_request("eth_sendTransaction", &[
      "{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
    ]);
    transport.assert_no_more_requests();
    assert_eq!(result, 5.into());
  }

  #[test]
  fn should_estimate_gas_usage() {
    // given
    let mut transport = TestTransport::default();
    transport.set_response(rpc::Value::String(format!("{:?}", U256::from(5))));

    let result = {
      let token = contract(&transport);

      // when
      token.estimate_gas("name", (), 5.into(), Options::default()).wait().unwrap()
    };

    // then
    transport.assert_request("eth_estimateGas", &[
      "{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
      "\"latest\"".into(),
    ]);
    transport.assert_no_more_requests();
    assert_eq!(result, 5.into());
  }

  #[test]
  fn should_query_single_parameter_function() {
    // given
    let mut transport = TestTransport::default();
    transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020".into()));

    let result: U256 = {
      let token = contract(&transport);

      // when
      token.query("balanceOf", Address::from(5), None, Options::default(), None).wait().unwrap()
    };

    // then
    transport.assert_request("eth_call", &[
      "{\"data\":\"0x70a082310000000000000000000000000000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
      "\"latest\"".into(),
    ]);
    transport.assert_no_more_requests();
    assert_eq!(result, 0x20.into());
  }
}