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};
#[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
}
}
#[derive(Debug)]
pub struct Contract<T: Transport> {
address: Address,
eth: Eth<T>,
abi: ethabi::Contract,
}
impl<T: Transport> Contract<T> {
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> {
pub fn new(eth: Eth<T>, address: Address, abi: ethabi::Contract) -> Self {
Contract {
address,
eth,
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 address(&self) -> Address {
self.address
}
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(¶ms.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)
}
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(¶ms.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| {
confirm::SendTransactionWithConfirmation::from_err(
self.eth.transport().clone(),
::error::ErrorKind::Decoder(format!("{:?}", e))
)
})
}
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(¶ms.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)
}
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(¶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))
}, 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() {
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(), BlockNumber::Number(1)).wait().unwrap()
};
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() {
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(|options| {
options.gas_price = Some(10_000_000.into());
}), BlockNumber::Latest).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(), None).wait().unwrap()
};
transport.assert_request("eth_call", &[
"{\"data\":\"0x70a082310000000000000000000000000000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
"\"latest\"".into(),
]);
transport.assert_no_more_requests();
assert_eq!(result, 0x20.into());
}
}