use ethabi;
use api::Eth;
use contract::result::QueryResult;
use contract::tokens::{Detokenize, Tokenize};
use types::{Address, Bytes, CallRequest, H256, TransactionRequest, TransactionCondition, U256};
use {Transport, Error as ApiError};
mod result;
pub mod tokens;
#[derive(Debug)]
pub enum Error {
Api(ApiError),
Abi(ethabi::Error),
InvalidOutputType(String),
}
impl From<ethabi::Error> for Error {
fn from(error: ethabi::Error) -> Self {
Error::Abi(error)
}
}
impl From<ApiError> for Error {
fn from(error: ApiError) -> Self {
Error::Api(error)
}
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Options {
pub gas: Option<U256>,
pub gas_price: Option<U256>,
pub value: Option<U256>,
pub nonce: Option<U256>,
pub condition: Option<TransactionCondition>,
}
impl Options {
pub fn with<F>(func: F) -> Options where
F: FnOnce(&mut Options)
{
let mut options = Options::default();
func(&mut options);
options
}
}
pub struct Contract<T: Transport> {
address: Address,
eth: Eth<T>,
abi: ethabi::Contract,
}
impl<T: Transport> Contract<T> {
pub fn new(eth: Eth<T>, address: Address, abi: ethabi::Contract) -> Self {
Contract {
address: address,
eth: eth,
abi: 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))
}
pub fn call<P>(&self, func: &str, params: P, from: Address, options: Options) -> QueryResult<H256, T::Out> where
P: Tokenize,
{
self.abi.function(func.into())
.and_then(|function| function.encode_input(¶ms.into_tokens()))
.map(move |data| {
let result = 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,
});
QueryResult::simple(result)
})
.unwrap_or_else(Into::into)
}
pub fn estimate_gas<P>(&self, func: &str, params: P, from: Address, options: Options) -> QueryResult<U256, T::Out> where
P: Tokenize,
{
self.abi.function(func.into())
.and_then(|function| function.encode_input(¶ms.into_tokens()))
.map(|data| {
let result = 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);
QueryResult::simple(result)
})
.unwrap_or_else(Into::into)
}
pub fn query<R, A, P>(&self, func: &str, params: P, from: A, options: Options) -> QueryResult<R, T::Out> where
R: Detokenize,
A: Into<Option<Address>>,
P: Tokenize,
{
self.abi.function(func.into())
.and_then(|function| function.encode_input(¶ms.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))
}, None);
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};
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() {
let mut transport = TestTransport::default();
transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
let result: String = {
let token = contract(&transport);
token.query("name", (), None, Options::default()).wait().unwrap()
};
transport.assert_request("eth_call", &[
"{\"data\":\"0x06fdde03\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
"\"latest\"".into(),
]);
transport.assert_no_more_requests();
assert_eq!(result, "Hello World!".to_owned());
}
#[test]
fn should_query_with_params() {
let mut transport = TestTransport::default();
transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
let result: String = {
let token = contract(&transport);
token.query("name", (), Address::from(5), Options::with(|mut options| {
options.gas_price = Some(10_000_000.into());
})).wait().unwrap()
};
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() {
let mut transport = TestTransport::default();
transport.set_response(rpc::Value::String(format!("{:?}", H256::from(5))));
let result = {
let token = contract(&transport);
token.call("name", (), 5.into(), Options::default()).wait().unwrap()
};
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() {
let mut transport = TestTransport::default();
transport.set_response(rpc::Value::String(format!("{:?}", U256::from(5))));
let result = {
let token = contract(&transport);
token.estimate_gas("name", (), 5.into(), Options::default()).wait().unwrap()
};
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() {
let mut transport = TestTransport::default();
transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020".into()));
let result: U256 = {
let token = contract(&transport);
token.query("balanceOf", (Address::from(5)), None, Options::default()).wait().unwrap()
};
transport.assert_request("eth_call", &[
"{\"data\":\"0x70a082310000000000000000000000000000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
"\"latest\"".into(),
]);
transport.assert_no_more_requests();
assert_eq!(result, 0x20.into());
}
}