1use crate::{
4 api::{Eth, Namespace},
5 confirm,
6 contract::tokens::{Detokenize, Tokenize},
7 futures::Future,
8 ic::KeyInfo,
9 transports::ic_http_client::CallOptions,
10 types::{
11 AccessList, Address, BlockId, Bytes, CallRequest, FilterBuilder, TransactionCondition, TransactionParameters,
12 TransactionReceipt, TransactionRequest, H256, U256, U64,
13 },
14 Transport,
15};
16use std::{collections::HashMap, hash::Hash, time};
17
18pub mod deploy;
19mod error;
21pub mod tokens;
22
23pub use crate::contract::error::Error;
24
25pub type Result<T> = std::result::Result<T, Error>;
27
28#[derive(Default, Debug, Clone, PartialEq)]
30pub struct Options {
31 pub gas: Option<U256>,
33 pub gas_price: Option<U256>,
35 pub value: Option<U256>,
37 pub nonce: Option<U256>,
39 pub condition: Option<TransactionCondition>,
41 pub transaction_type: Option<U64>,
43 pub access_list: Option<AccessList>,
45 pub max_fee_per_gas: Option<U256>,
47 pub max_priority_fee_per_gas: Option<U256>,
49 pub call_options: Option<CallOptions>,
50}
51
52impl Options {
53 pub fn with<F>(func: F) -> Options
55 where
56 F: FnOnce(&mut Options),
57 {
58 let mut options = Options::default();
59 func(&mut options);
60 options
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct Contract<T: Transport> {
67 address: Address,
68 eth: Eth<T>,
69 abi: ethabi::Contract,
70}
71
72impl<T: Transport> Contract<T> {
73 }
107
108impl<T: Transport> Contract<T> {
109 pub fn new(eth: Eth<T>, address: Address, abi: ethabi::Contract) -> Self {
111 Contract { address, eth, abi }
112 }
113
114 pub fn from_json(eth: Eth<T>, address: Address, json: &[u8]) -> ethabi::Result<Self> {
116 let abi = ethabi::Contract::load(json)?;
117 Ok(Self::new(eth, address, abi))
118 }
119
120 pub fn abi(&self) -> ðabi::Contract {
122 &self.abi
123 }
124
125 pub fn address(&self) -> Address {
127 self.address
128 }
129
130 pub async fn call<P>(&self, func: &str, params: P, from: Address, options: Options) -> Result<H256>
132 where
133 P: Tokenize,
134 {
135 let data = self.abi.function(func)?.encode_input(¶ms.into_tokens())?;
136 let Options {
137 gas,
138 gas_price,
139 value,
140 nonce,
141 condition,
142 transaction_type,
143 access_list,
144 max_fee_per_gas,
145 max_priority_fee_per_gas,
146 call_options,
147 } = options;
148 self.eth
149 .send_transaction(
150 TransactionRequest {
151 from,
152 to: Some(self.address),
153 gas,
154 gas_price,
155 value,
156 nonce,
157 data: Some(Bytes(data)),
158 condition,
159 transaction_type,
160 access_list,
161 max_fee_per_gas,
162 max_priority_fee_per_gas,
163 },
164 call_options.unwrap_or_default(),
165 )
166 .await
167 .map_err(Error::from)
168 }
169
170 pub async fn call_with_confirmations(
172 &self,
173 func: &str,
174 params: impl Tokenize,
175 from: Address,
176 options: Options,
177 confirmations: usize,
178 ) -> crate::error::Result<TransactionReceipt> {
179 let poll_interval = time::Duration::from_secs(1);
180
181 let fn_data = self
182 .abi
183 .function(func)
184 .and_then(|function| function.encode_input(¶ms.into_tokens()))
185 .map_err(|err| crate::error::Error::Decoder(format!("{:?}", err)))?;
188 let transaction_request = TransactionRequest {
189 from,
190 to: Some(self.address),
191 gas: options.gas,
192 gas_price: options.gas_price,
193 value: options.value,
194 nonce: options.nonce,
195 data: Some(Bytes(fn_data)),
196 condition: options.condition,
197 transaction_type: options.transaction_type,
198 access_list: options.access_list,
199 max_fee_per_gas: options.max_fee_per_gas,
200 max_priority_fee_per_gas: options.max_priority_fee_per_gas,
201 };
202 confirm::send_transaction_with_confirmation(
203 self.eth.transport().clone(),
204 transaction_request,
205 poll_interval,
206 confirmations,
207 options.call_options.unwrap_or_default(),
208 )
209 .await
210 }
211
212 pub async fn estimate_gas<P>(&self, func: &str, params: P, from: Address, options: Options) -> Result<U256>
214 where
215 P: Tokenize,
216 {
217 let data = self.abi.function(func)?.encode_input(¶ms.into_tokens())?;
218 self.eth
219 .estimate_gas(
220 CallRequest {
221 from: Some(from),
222 to: Some(self.address),
223 gas: options.gas,
224 gas_price: options.gas_price,
225 value: options.value,
226 data: Some(Bytes(data)),
227 transaction_type: options.transaction_type,
228 access_list: options.access_list,
229 max_fee_per_gas: options.max_fee_per_gas,
230 max_priority_fee_per_gas: options.max_priority_fee_per_gas,
231 },
232 None,
233 options.call_options.unwrap_or_default(),
234 )
235 .await
236 .map_err(Into::into)
237 }
238 pub async fn _estimate_gas(
239 &self,
240 from: Address,
241 tx: &TransactionParameters,
242 call_options: CallOptions,
243 ) -> crate::Result<U256> {
244 self.eth
245 .estimate_gas(
246 CallRequest {
247 from: Some(from),
248 to: tx.to,
249 gas: None,
250 gas_price: tx.gas_price,
251 value: Some(tx.value),
252 data: Some(tx.data.clone()),
253 transaction_type: tx.transaction_type,
254 access_list: tx.access_list.clone(),
255 max_fee_per_gas: tx.max_fee_per_gas,
256 max_priority_fee_per_gas: tx.max_priority_fee_per_gas,
257 },
258 None,
259 call_options,
260 )
261 .await
262 }
263
264 pub fn query<R, A, B, P>(
266 &self,
267 func: &str,
268 params: P,
269 from: A,
270 options: Options,
271 block: B,
272 ) -> impl Future<Output = Result<R>> + '_
273 where
274 R: Detokenize,
275 A: Into<Option<Address>>,
276 B: Into<Option<BlockId>>,
277 P: Tokenize,
278 {
279 let result = self
280 .abi
281 .function(func)
282 .and_then(|function| {
283 function
284 .encode_input(¶ms.into_tokens())
285 .map(|call| (call, function))
286 })
287 .map(|(call, function)| {
288 let call_future = self.eth.call(
289 CallRequest {
290 from: from.into(),
291 to: Some(self.address),
292 gas: options.gas,
293 gas_price: options.gas_price,
294 value: options.value,
295 data: Some(Bytes(call)),
296 transaction_type: options.transaction_type,
297 access_list: options.access_list,
298 max_fee_per_gas: options.max_fee_per_gas,
299 max_priority_fee_per_gas: options.max_priority_fee_per_gas,
300 },
301 block.into(),
302 options.call_options.unwrap_or_default(),
303 );
304 (call_future, function)
305 });
306 async {
309 let (call_future, function) = result?;
310 let bytes = call_future.await?;
311 let output = function.decode_output(&bytes.0)?;
312 R::from_tokens(output)
313 }
314 }
315
316 pub async fn events<A, B, C, R>(
318 &self,
319 event: &str,
320 topic0: A,
321 topic1: B,
322 topic2: C,
323 options: CallOptions,
324 ) -> Result<Vec<R>>
325 where
326 A: Tokenize,
327 B: Tokenize,
328 C: Tokenize,
329 R: Detokenize,
330 {
331 fn to_topic<A: Tokenize>(x: A) -> ethabi::Topic<ethabi::Token> {
332 let tokens = x.into_tokens();
333 if tokens.is_empty() {
334 ethabi::Topic::Any
335 } else {
336 tokens.into()
337 }
338 }
339
340 let res = self.abi.event(event).and_then(|ev| {
341 let filter = ev.filter(ethabi::RawTopicFilter {
342 topic0: to_topic(topic0),
343 topic1: to_topic(topic1),
344 topic2: to_topic(topic2),
345 })?;
346 Ok((ev.clone(), filter))
347 });
348 let (ev, filter) = match res {
349 Ok(x) => x,
350 Err(e) => return Err(e.into()),
351 };
352
353 let logs = self
354 .eth
355 .logs(FilterBuilder::default().topic_filter(filter).build(), options)
356 .await?;
357 logs.into_iter()
358 .map(move |l| {
359 let log = ev.parse_log(ethabi::RawLog {
360 topics: l.topics,
361 data: l.data.0,
362 })?;
363
364 R::from_tokens(log.params.into_iter().map(|x| x.value).collect::<Vec<_>>())
365 })
366 .collect::<Result<Vec<R>>>()
367 }
368}
369
370mod contract_signing {
372 use std::str::FromStr;
373
374 use super::*;
375 use crate::{
376 api::Accounts,
377 types::{SignedTransaction, TransactionParameters},
378 };
379
380 impl<T: Transport> Contract<T> {
381 pub async fn sign(
382 &self,
383 func: &str,
384 params: impl Tokenize,
385 options: Options,
386 from: String,
387 key_info: KeyInfo,
388 chain_id: u64,
389 ) -> crate::Result<SignedTransaction> {
390 let fn_data = self
391 .abi
392 .function(func)
393 .and_then(|function| function.encode_input(¶ms.into_tokens()))
394 .map_err(|err| crate::error::Error::Decoder(format!("{:?}", err)))?;
397 let accounts = Accounts::new(self.eth.transport().clone());
398 let mut tx = TransactionParameters {
399 nonce: options.nonce,
400 to: Some(self.address),
401 gas_price: options.gas_price,
402 data: Bytes(fn_data),
403 transaction_type: options.transaction_type,
404 access_list: options.access_list,
405 max_fee_per_gas: options.max_fee_per_gas,
406 max_priority_fee_per_gas: options.max_priority_fee_per_gas,
407 ..Default::default()
408 };
409 tx.gas = if let Some(gas) = options.gas {
410 gas
411 } else {
412 self._estimate_gas(
413 Address::from_str(&from).unwrap(),
414 &tx,
415 options.call_options.unwrap_or_default(),
416 )
417 .await?
418 };
419 if let Some(value) = options.value {
420 tx.value = value;
421 }
422 accounts.sign_transaction(tx, from, key_info, chain_id).await
423 }
424
425 pub async fn signed_call(
430 &self,
431 func: &str,
432 params: impl Tokenize,
433 options: Options,
434 from: String,
435 key_info: KeyInfo,
436 chain_id: u64,
437 ) -> crate::Result<H256> {
438 let signed = self
439 .sign(
440 func,
441 params,
442 Options {
443 call_options: None,
444 ..options.clone()
445 },
446 from,
447 key_info,
448 chain_id,
449 )
450 .await?;
451 self.eth
452 .send_raw_transaction(signed.raw_transaction, options.call_options.unwrap_or_default())
453 .await?;
454 Ok(signed.transaction_hash)
455 }
456
457 pub async fn signed_call_with_confirmations(
462 &self,
463 func: &str,
464 params: impl Tokenize,
465 options: Options,
466 from: String,
467 confirmations: usize,
468 key_info: KeyInfo,
469 chain_id: u64,
470 ) -> crate::Result<TransactionReceipt> {
471 let poll_interval = time::Duration::from_secs(1);
472 let signed = self
473 .sign(
474 func,
475 params,
476 Options {
477 call_options: None,
478 ..options.clone()
479 },
480 from,
481 key_info,
482 chain_id,
483 )
484 .await?;
485
486 confirm::send_raw_transaction_with_confirmation(
487 self.eth.transport().clone(),
488 signed.raw_transaction,
489 poll_interval,
490 confirmations,
491 options.call_options.unwrap_or_default(),
492 )
493 .await
494 }
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::{Contract, Options};
501 use crate::{
502 api::{self, Namespace},
503 rpc,
504 transports::test::TestTransport,
505 types::{Address, BlockId, BlockNumber, H256, U256},
506 Transport,
507 };
508
509 fn contract<T: Transport>(transport: &T) -> Contract<&T> {
510 let eth = api::Eth::new(transport);
511 Contract::from_json(eth, Address::from_low_u64_be(1), include_bytes!("./res/token.json")).unwrap()
512 }
513
514 #[test]
515 fn should_call_constant_function() {
516 let mut transport = TestTransport::default();
518 transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
519
520 let result: String = {
521 let token = contract(&transport);
522
523 futures::executor::block_on(token.query(
525 "name",
526 (),
527 None,
528 Options::default(),
529 BlockId::Number(BlockNumber::Number(1.into())),
530 ))
531 .unwrap()
532 };
533
534 transport.assert_request(
536 "eth_call",
537 &[
538 "{\"data\":\"0x06fdde03\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
539 "\"0x1\"".into(),
540 ],
541 );
542 transport.assert_no_more_requests();
543 assert_eq!(result, "Hello World!".to_owned());
544 }
545
546 #[test]
547 fn should_call_constant_function_by_hash() {
548 let mut transport = TestTransport::default();
550 transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
551
552 let result: String = {
553 let token = contract(&transport);
554
555 futures::executor::block_on(token.query(
557 "name",
558 (),
559 None,
560 Options::default(),
561 BlockId::Hash(H256::default()),
562 ))
563 .unwrap()
564 };
565
566 transport.assert_request(
568 "eth_call",
569 &[
570 "{\"data\":\"0x06fdde03\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
571 "{\"blockHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}".into(),
572 ],
573 );
574 transport.assert_no_more_requests();
575 assert_eq!(result, "Hello World!".to_owned());
576 }
577
578 #[test]
579 fn should_query_with_params() {
580 let mut transport = TestTransport::default();
582 transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
583
584 let result: String = {
585 let token = contract(&transport);
586
587 futures::executor::block_on(token.query(
589 "name",
590 (),
591 Address::from_low_u64_be(5),
592 Options::with(|options| {
593 options.gas_price = Some(10_000_000.into());
594 }),
595 BlockId::Number(BlockNumber::Latest),
596 ))
597 .unwrap()
598 };
599
600 transport.assert_request("eth_call", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"gasPrice\":\"0x989680\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(), "\"latest\"".into()]);
602 transport.assert_no_more_requests();
603 assert_eq!(result, "Hello World!".to_owned());
604 }
605
606 #[test]
607 fn should_call_a_contract_function() {
608 let mut transport = TestTransport::default();
610 transport.set_response(rpc::Value::String(format!("{:?}", H256::from_low_u64_be(5))));
611
612 let result = {
613 let token = contract(&transport);
614
615 futures::executor::block_on(token.call("name", (), Address::from_low_u64_be(5), Options::default()))
617 .unwrap()
618 };
619
620 transport.assert_request("eth_sendTransaction", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into()]);
622 transport.assert_no_more_requests();
623 assert_eq!(result, H256::from_low_u64_be(5));
624 }
625
626 #[test]
627 fn should_estimate_gas_usage() {
628 let mut transport = TestTransport::default();
630 transport.set_response(rpc::Value::String(format!("{:#x}", U256::from(5))));
631
632 let result = {
633 let token = contract(&transport);
634
635 futures::executor::block_on(token.estimate_gas("name", (), Address::from_low_u64_be(5), Options::default()))
637 .unwrap()
638 };
639
640 transport.assert_request("eth_estimateGas", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into()]);
642 transport.assert_no_more_requests();
643 assert_eq!(result, 5.into());
644 }
645
646 #[test]
647 fn should_query_single_parameter_function() {
648 let mut transport = TestTransport::default();
650 transport.set_response(rpc::Value::String(
651 "0x0000000000000000000000000000000000000000000000000000000000000020".into(),
652 ));
653
654 let result: U256 = {
655 let token = contract(&transport);
656
657 futures::executor::block_on(token.query(
659 "balanceOf",
660 Address::from_low_u64_be(5),
661 None,
662 Options::default(),
663 None,
664 ))
665 .unwrap()
666 };
667
668 transport.assert_request("eth_call", &["{\"data\":\"0x70a082310000000000000000000000000000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(), "\"latest\"".into()]);
670 transport.assert_no_more_requests();
671 assert_eq!(result, 0x20.into());
672 }
673}