use crate::{
api::{Eth, Namespace},
confirm,
contract::tokens::{Detokenize, Tokenize},
futures::Future,
ic::KeyInfo,
transports::ic_http_client::CallOptions,
types::{
AccessList, Address, BlockId, Bytes, CallRequest, FilterBuilder, TransactionCondition, TransactionParameters,
TransactionReceipt, TransactionRequest, H256, U256, U64,
},
Transport,
};
use std::{collections::HashMap, hash::Hash, time};
pub mod deploy;
mod error;
pub mod tokens;
pub use crate::contract::error::Error;
pub type Result<T> = std::result::Result<T, 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>,
pub transaction_type: Option<U64>,
pub access_list: Option<AccessList>,
pub max_fee_per_gas: Option<U256>,
pub max_priority_fee_per_gas: Option<U256>,
pub call_options: Option<CallOptions>,
}
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, Clone)]
pub struct Contract<T: Transport> {
address: Address,
eth: Eth<T>,
abi: ethabi::Contract,
}
impl<T: Transport> Contract<T> {
}
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]) -> ethabi::Result<Self> {
let abi = ethabi::Contract::load(json)?;
Ok(Self::new(eth, address, abi))
}
pub fn abi(&self) -> ðabi::Contract {
&self.abi
}
pub fn address(&self) -> Address {
self.address
}
pub async fn call<P>(&self, func: &str, params: P, from: Address, options: Options) -> Result<H256>
where
P: Tokenize,
{
let data = self.abi.function(func)?.encode_input(¶ms.into_tokens())?;
let Options {
gas,
gas_price,
value,
nonce,
condition,
transaction_type,
access_list,
max_fee_per_gas,
max_priority_fee_per_gas,
call_options,
} = options;
self.eth
.send_transaction(
TransactionRequest {
from,
to: Some(self.address),
gas,
gas_price,
value,
nonce,
data: Some(Bytes(data)),
condition,
transaction_type,
access_list,
max_fee_per_gas,
max_priority_fee_per_gas,
},
call_options.unwrap_or_default(),
)
.await
.map_err(Error::from)
}
pub async fn call_with_confirmations(
&self,
func: &str,
params: impl Tokenize,
from: Address,
options: Options,
confirmations: usize,
) -> crate::error::Result<TransactionReceipt> {
let poll_interval = time::Duration::from_secs(1);
let fn_data = self
.abi
.function(func)
.and_then(|function| function.encode_input(¶ms.into_tokens()))
.map_err(|err| crate::error::Error::Decoder(format!("{:?}", err)))?;
let transaction_request = TransactionRequest {
from,
to: Some(self.address),
gas: options.gas,
gas_price: options.gas_price,
value: options.value,
nonce: options.nonce,
data: Some(Bytes(fn_data)),
condition: options.condition,
transaction_type: options.transaction_type,
access_list: options.access_list,
max_fee_per_gas: options.max_fee_per_gas,
max_priority_fee_per_gas: options.max_priority_fee_per_gas,
};
confirm::send_transaction_with_confirmation(
self.eth.transport().clone(),
transaction_request,
poll_interval,
confirmations,
options.call_options.unwrap_or_default(),
)
.await
}
pub async fn estimate_gas<P>(&self, func: &str, params: P, from: Address, options: Options) -> Result<U256>
where
P: Tokenize,
{
let data = self.abi.function(func)?.encode_input(¶ms.into_tokens())?;
self.eth
.estimate_gas(
CallRequest {
from: Some(from),
to: Some(self.address),
gas: options.gas,
gas_price: options.gas_price,
value: options.value,
data: Some(Bytes(data)),
transaction_type: options.transaction_type,
access_list: options.access_list,
max_fee_per_gas: options.max_fee_per_gas,
max_priority_fee_per_gas: options.max_priority_fee_per_gas,
},
None,
options.call_options.unwrap_or_default(),
)
.await
.map_err(Into::into)
}
pub async fn _estimate_gas(
&self,
from: Address,
tx: &TransactionParameters,
call_options: CallOptions,
) -> Result<U256> {
self.eth
.estimate_gas(
CallRequest {
from: Some(from),
to: tx.to,
gas: None,
gas_price: tx.gas_price,
value: Some(tx.value),
data: Some(tx.data.clone()),
transaction_type: tx.transaction_type,
access_list: tx.access_list.clone(),
max_fee_per_gas: tx.max_fee_per_gas,
max_priority_fee_per_gas: tx.max_priority_fee_per_gas,
},
None,
call_options,
)
.await
.map_err(Into::into)
}
pub fn query<R, A, B, P>(
&self,
func: &str,
params: P,
from: A,
options: Options,
block: B,
) -> impl Future<Output = Result<R>> + '_
where
R: Detokenize,
A: Into<Option<Address>>,
B: Into<Option<BlockId>>,
P: Tokenize,
{
let result = self
.abi
.function(func)
.and_then(|function| {
function
.encode_input(¶ms.into_tokens())
.map(|call| (call, function))
})
.map(|(call, function)| {
let call_future = self.eth.call(
CallRequest {
from: from.into(),
to: Some(self.address),
gas: options.gas,
gas_price: options.gas_price,
value: options.value,
data: Some(Bytes(call)),
transaction_type: options.transaction_type,
access_list: options.access_list,
max_fee_per_gas: options.max_fee_per_gas,
max_priority_fee_per_gas: options.max_priority_fee_per_gas,
},
block.into(),
options.call_options.unwrap_or_default(),
);
(call_future, function)
});
async {
let (call_future, function) = result?;
let bytes = call_future.await?;
let output = function.decode_output(&bytes.0)?;
R::from_tokens(output)
}
}
pub async fn events<A, B, C, R>(
&self,
event: &str,
topic0: A,
topic1: B,
topic2: C,
options: CallOptions,
) -> Result<Vec<R>>
where
A: Tokenize,
B: Tokenize,
C: Tokenize,
R: Detokenize,
{
fn to_topic<A: Tokenize>(x: A) -> ethabi::Topic<ethabi::Token> {
let tokens = x.into_tokens();
if tokens.is_empty() {
ethabi::Topic::Any
} else {
tokens.into()
}
}
let res = self.abi.event(event).and_then(|ev| {
let filter = ev.filter(ethabi::RawTopicFilter {
topic0: to_topic(topic0),
topic1: to_topic(topic1),
topic2: to_topic(topic2),
})?;
Ok((ev.clone(), filter))
});
let (ev, filter) = match res {
Ok(x) => x,
Err(e) => return Err(e.into()),
};
let logs = self
.eth
.logs(FilterBuilder::default().topic_filter(filter).build(), options)
.await?;
logs.into_iter()
.map(move |l| {
let log = ev.parse_log(ethabi::RawLog {
topics: l.topics,
data: l.data.0,
})?;
R::from_tokens(log.params.into_iter().map(|x| x.value).collect::<Vec<_>>())
})
.collect::<Result<Vec<R>>>()
}
}
mod contract_signing {
use std::str::FromStr;
use super::*;
use crate::{
api::Accounts,
types::{SignedTransaction, TransactionParameters},
};
impl<T: Transport> Contract<T> {
pub async fn sign(
&self,
func: &str,
params: impl Tokenize,
options: Options,
from: String,
key_info: KeyInfo,
chain_id: u64,
) -> crate::Result<SignedTransaction> {
let fn_data = self
.abi
.function(func)
.and_then(|function| function.encode_input(¶ms.into_tokens()))
.map_err(|err| crate::error::Error::Decoder(format!("{:?}", err)))?;
let accounts = Accounts::new(self.eth.transport().clone());
let mut tx = TransactionParameters {
nonce: options.nonce,
to: Some(self.address),
gas_price: options.gas_price,
data: Bytes(fn_data),
transaction_type: options.transaction_type,
access_list: options.access_list,
max_fee_per_gas: options.max_fee_per_gas,
max_priority_fee_per_gas: options.max_priority_fee_per_gas,
..Default::default()
};
tx.gas = if let Some(gas) = options.gas {
gas
} else {
match self
._estimate_gas(
Address::from_str(&from.to_string().as_str()).unwrap(),
&tx,
options.call_options.unwrap_or_default(),
)
.await
{
Ok(gas) => gas,
Err(e) => {
return Err(e.into());
}
}
};
if let Some(value) = options.value {
tx.value = value;
}
accounts.sign_transaction(tx, from, key_info, chain_id).await
}
pub async fn signed_call(
&self,
func: &str,
params: impl Tokenize,
options: Options,
from: String,
key_info: KeyInfo,
chain_id: u64,
) -> crate::Result<H256> {
let signed = self
.sign(
func,
params,
Options {
call_options: None,
..options.clone()
},
from,
key_info,
chain_id,
)
.await?;
self.eth
.send_raw_transaction(signed.raw_transaction, options.call_options.unwrap_or_default())
.await?;
Ok(signed.transaction_hash)
}
pub async fn signed_call_with_confirmations(
&self,
func: &str,
params: impl Tokenize,
options: Options,
from: String,
confirmations: usize,
key_info: KeyInfo,
chain_id: u64,
) -> crate::Result<TransactionReceipt> {
let poll_interval = time::Duration::from_secs(1);
let signed = self
.sign(
func,
params,
Options {
call_options: None,
..options.clone()
},
from,
key_info,
chain_id,
)
.await?;
confirm::send_raw_transaction_with_confirmation(
self.eth.transport().clone(),
signed.raw_transaction,
poll_interval,
confirmations,
options.call_options.unwrap_or_default(),
)
.await
}
}
}
#[cfg(test)]
mod tests {
use super::{Contract, Options};
use crate::{
api::{self, Namespace},
rpc,
transports::test::TestTransport,
types::{Address, BlockId, BlockNumber, H256, U256},
Transport,
};
fn contract<T: Transport>(transport: &T) -> Contract<&T> {
let eth = api::Eth::new(transport);
Contract::from_json(eth, Address::from_low_u64_be(1), 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);
futures::executor::block_on(token.query(
"name",
(),
None,
Options::default(),
BlockId::Number(BlockNumber::Number(1.into())),
))
.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_call_constant_function_by_hash() {
let mut transport = TestTransport::default();
transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
let result: String = {
let token = contract(&transport);
futures::executor::block_on(token.query(
"name",
(),
None,
Options::default(),
BlockId::Hash(H256::default()),
))
.unwrap()
};
transport.assert_request(
"eth_call",
&[
"{\"data\":\"0x06fdde03\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
"{\"blockHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}".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);
futures::executor::block_on(token.query(
"name",
(),
Address::from_low_u64_be(5),
Options::with(|options| {
options.gas_price = Some(10_000_000.into());
}),
BlockId::Number(BlockNumber::Latest),
))
.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_low_u64_be(5))));
let result = {
let token = contract(&transport);
futures::executor::block_on(token.call("name", (), Address::from_low_u64_be(5), Options::default()))
.unwrap()
};
transport.assert_request("eth_sendTransaction", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into()]);
transport.assert_no_more_requests();
assert_eq!(result, H256::from_low_u64_be(5));
}
#[test]
fn should_estimate_gas_usage() {
let mut transport = TestTransport::default();
transport.set_response(rpc::Value::String(format!("{:#x}", U256::from(5))));
let result = {
let token = contract(&transport);
futures::executor::block_on(token.estimate_gas("name", (), Address::from_low_u64_be(5), Options::default()))
.unwrap()
};
transport.assert_request("eth_estimateGas", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".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);
futures::executor::block_on(token.query(
"balanceOf",
Address::from_low_u64_be(5),
None,
Options::default(),
None,
))
.unwrap()
};
transport.assert_request("eth_call", &["{\"data\":\"0x70a082310000000000000000000000000000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(), "\"latest\"".into()]);
transport.assert_no_more_requests();
assert_eq!(result, 0x20.into());
}
}