feat: add tokenization and improve contract API for events
This commit is contained in:
parent
33b36bbc52
commit
d3b9b378c5
|
@ -244,6 +244,7 @@ dependencies = [
|
||||||
name = "ethers"
|
name = "ethers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bincode",
|
"bincode",
|
||||||
"ethabi",
|
"ethabi",
|
||||||
|
|
|
@ -22,6 +22,7 @@ solc = { git = "https://github.com/paritytech/rust_solc "}
|
||||||
rlp = "0.4.5"
|
rlp = "0.4.5"
|
||||||
ethabi = "12.0.0"
|
ethabi = "12.0.0"
|
||||||
bincode = "1.2.1"
|
bincode = "1.2.1"
|
||||||
|
arrayvec = "0.5.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "0.2.21", features = ["macros"] }
|
tokio = { version = "0.2.21", features = ["macros"] }
|
||||||
|
|
|
@ -12,6 +12,14 @@ Complete Ethereum wallet implementation and utilities in Rust (with WASM and FFI
|
||||||
- [ ] Hardware wallet support
|
- [ ] Hardware wallet support
|
||||||
- [ ] ...
|
- [ ] ...
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
This library would not have been possibly without the great work of the creators of [`rust-web3`]() and [`ethcontract-rs`]()
|
||||||
|
|
||||||
|
A lot of the code was inspired and adapted from them, to a unified and opinionated interface.
|
||||||
|
That said, Rust-web3 is ~9k LoC (tests included) and ethcontract-rs is 11k lines,
|
||||||
|
so in total about 20k lines of code with tests. This library is xxx LoC.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Sending a transaction with an offline key
|
### Sending a transaction with an offline key
|
||||||
|
|
|
@ -1,11 +1,38 @@
|
||||||
|
use ethabi::Token;
|
||||||
use ethers::{
|
use ethers::{
|
||||||
abi::ParamType,
|
contract::{Contract, Detokenize},
|
||||||
contract::Contract,
|
types::Address,
|
||||||
types::{Address, Filter},
|
|
||||||
HttpProvider, MainnetWallet,
|
HttpProvider, MainnetWallet,
|
||||||
};
|
};
|
||||||
|
use serde::Serialize;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
const ABI: &'static str = r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
// TODO: This should be `derive`-able on such types -> similar to how Zexe's Deserialize is done
|
||||||
|
struct ValueChanged {
|
||||||
|
author: Address,
|
||||||
|
old_value: String,
|
||||||
|
new_value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Detokenize for ValueChanged {
|
||||||
|
fn from_tokens(
|
||||||
|
tokens: Vec<Token>,
|
||||||
|
) -> Result<ValueChanged, ethers::contract::InvalidOutputType> {
|
||||||
|
let author: Address = tokens[0].clone().to_address().unwrap();
|
||||||
|
let old_value = tokens[1].clone().to_string().unwrap();
|
||||||
|
let new_value = tokens[2].clone().to_string().unwrap();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
author,
|
||||||
|
old_value,
|
||||||
|
new_value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), failure::Error> {
|
async fn main() -> Result<(), failure::Error> {
|
||||||
// connect to the network
|
// connect to the network
|
||||||
|
@ -21,50 +48,18 @@ async fn main() -> Result<(), failure::Error> {
|
||||||
// get the contract's address
|
// get the contract's address
|
||||||
let addr = "683BEE23D79A1D8664dF70714edA966e1484Fd3d".parse::<Address>()?;
|
let addr = "683BEE23D79A1D8664dF70714edA966e1484Fd3d".parse::<Address>()?;
|
||||||
|
|
||||||
// get the contract's ABI
|
|
||||||
let abi = r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#;
|
|
||||||
|
|
||||||
// instantiate it
|
// instantiate it
|
||||||
let contract = Contract::new(&client, serde_json::from_str(abi)?, addr);
|
let contract = Contract::new(&client, serde_json::from_str(ABI)?, addr);
|
||||||
|
|
||||||
// get the args
|
|
||||||
let event = "ValueChanged(address,string,string)";
|
|
||||||
|
|
||||||
let args = &[ethabi::Token::String("hello!".to_owned())];
|
|
||||||
|
|
||||||
// call the method
|
// call the method
|
||||||
let tx_hash = contract.method("setValue", args)?.send().await?;
|
let _tx_hash = contract.method("setValue", "hi".to_owned())?.send().await?;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
let logs: Vec<ValueChanged> = contract
|
||||||
struct ValueChanged {
|
.event("ValueChanged")?
|
||||||
author: Address,
|
.from_block(0u64)
|
||||||
old_value: String,
|
.query()
|
||||||
new_value: String,
|
.await?;
|
||||||
}
|
|
||||||
|
|
||||||
let filter = Filter::new().from_block(0).address(addr).event(event);
|
println!("{}", serde_json::to_string(&logs)?);
|
||||||
let logs = provider
|
|
||||||
.get_logs(&filter)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|log| {
|
|
||||||
// decode the non-indexed data
|
|
||||||
let data = ethabi::decode(&[ParamType::String, ParamType::String], log.data.as_ref())?;
|
|
||||||
|
|
||||||
let author = log.topics[1].into();
|
|
||||||
|
|
||||||
// Unwrap?
|
|
||||||
let old_value = data[0].clone().to_string().unwrap();
|
|
||||||
let new_value = data[1].clone().to_string().unwrap();
|
|
||||||
|
|
||||||
Ok(ValueChanged {
|
|
||||||
old_value,
|
|
||||||
new_value,
|
|
||||||
author,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, ethabi::Error>>()?;
|
|
||||||
|
|
||||||
dbg!(logs);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,8 @@ impl EventExt for Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tokenization macros: Given ABI -> codegen: copy Gnosis' thing
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
abi::{Abi, Function, FunctionExt},
|
abi::{self, Abi, EventExt, Function, FunctionExt},
|
||||||
providers::JsonRpcClient,
|
contract::{Detokenize, Tokenize},
|
||||||
|
providers::{JsonRpcClient, Provider},
|
||||||
signers::{Client, Signer},
|
signers::{Client, Signer},
|
||||||
types::{Address, BlockNumber, Selector, TransactionRequest, H256, U256},
|
types::{Address, BlockNumber, Filter, Selector, TransactionRequest, ValueOrArray, H256, U256},
|
||||||
};
|
};
|
||||||
|
|
||||||
use rustc_hex::ToHex;
|
use rustc_hex::ToHex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{collections::HashMap, hash::Hash};
|
use std::{collections::HashMap, fmt::Debug, hash::Hash};
|
||||||
|
|
||||||
/// Represents a contract instance at an address. Provides methods for
|
/// Represents a contract instance at an address. Provides methods for
|
||||||
/// contract interaction.
|
/// contract interaction.
|
||||||
|
@ -20,36 +20,43 @@ pub struct Contract<'a, S, P> {
|
||||||
/// A mapping from method signature to a name-index pair for accessing
|
/// A mapping from method signature to a name-index pair for accessing
|
||||||
/// functions in the contract ABI. This is used to avoid allocation when
|
/// functions in the contract ABI. This is used to avoid allocation when
|
||||||
/// searching for matching functions by signature.
|
/// searching for matching functions by signature.
|
||||||
|
// Adapted from: https://github.com/gnosis/ethcontract-rs/blob/master/src/contract.rs
|
||||||
methods: HashMap<Selector, (String, usize)>,
|
methods: HashMap<Selector, (String, usize)>,
|
||||||
|
|
||||||
/// A mapping from event signature to a name-index pair for resolving
|
|
||||||
/// events in the contract ABI.
|
|
||||||
events: HashMap<H256, (String, usize)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S, P> Contract<'a, S, P> {
|
impl<'a, S, P> Contract<'a, S, P> {
|
||||||
/// Creates a new contract from the provided client, abi and address
|
/// Creates a new contract from the provided client, abi and address
|
||||||
pub fn new(client: &'a Client<'a, S, P>, abi: Abi, address: Address) -> Self {
|
pub fn new(client: &'a Client<'a, S, P>, abi: Abi, address: Address) -> Self {
|
||||||
let methods = create_mapping(&abi.functions, |function| function.selector());
|
let methods = create_mapping(&abi.functions, |function| function.selector());
|
||||||
let events = create_mapping(&abi.events, |event| event.signature());
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
abi,
|
abi,
|
||||||
address,
|
address,
|
||||||
methods,
|
methods,
|
||||||
events,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a transaction builder for the provided function name. If there are
|
/// Returns a transaction builder for the provided function name. If there are
|
||||||
/// multiple functions with the same name due to overloading, consider using
|
/// multiple functions with the same name due to overloading, consider using
|
||||||
/// the `method_hash` method instead, since this will use the first match.
|
/// the `method_hash` method instead, since this will use the first match.
|
||||||
pub fn method(
|
pub fn event<'b>(&'a self, name: &str) -> Result<Event<'a, 'b, P>, abi::Error>
|
||||||
&self,
|
where
|
||||||
name: &str,
|
'a: 'b,
|
||||||
args: &[ethabi::Token],
|
{
|
||||||
) -> Result<Sender<'a, S, P>, ethabi::Error> {
|
// get the event's full name
|
||||||
|
let event = self.abi.event(name)?;
|
||||||
|
Ok(Event {
|
||||||
|
provider: &self.client.provider,
|
||||||
|
filter: Filter::new().event(&event.abi_signature()),
|
||||||
|
event: &event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a transaction builder for the provided function name. If there are
|
||||||
|
/// multiple functions with the same name due to overloading, consider using
|
||||||
|
/// the `method_hash` method instead, since this will use the first match.
|
||||||
|
pub fn method<T: Tokenize>(&self, name: &str, args: T) -> Result<Sender<'a, S, P>, abi::Error> {
|
||||||
// get the function
|
// get the function
|
||||||
let function = self.abi.function(name)?;
|
let function = self.abi.function(name)?;
|
||||||
self.method_func(function, args)
|
self.method_func(function, args)
|
||||||
|
@ -57,26 +64,26 @@ impl<'a, S, P> Contract<'a, S, P> {
|
||||||
|
|
||||||
/// Returns a transaction builder for the selected function signature. This should be
|
/// Returns a transaction builder for the selected function signature. This should be
|
||||||
/// preferred if there are overloaded functions in your smart contract
|
/// preferred if there are overloaded functions in your smart contract
|
||||||
pub fn method_hash(
|
pub fn method_hash<T: Tokenize>(
|
||||||
&self,
|
&self,
|
||||||
signature: Selector,
|
signature: Selector,
|
||||||
args: &[ethabi::Token],
|
args: T,
|
||||||
) -> Result<Sender<'a, S, P>, ethabi::Error> {
|
) -> Result<Sender<'a, S, P>, abi::Error> {
|
||||||
let function = self
|
let function = self
|
||||||
.methods
|
.methods
|
||||||
.get(&signature)
|
.get(&signature)
|
||||||
.map(|(name, index)| &self.abi.functions[name][*index])
|
.map(|(name, index)| &self.abi.functions[name][*index])
|
||||||
.ok_or_else(|| ethabi::Error::InvalidName(signature.to_hex::<String>()))?;
|
.ok_or_else(|| abi::Error::InvalidName(signature.to_hex::<String>()))?;
|
||||||
self.method_func(function, args)
|
self.method_func(function, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn method_func(
|
fn method_func<T: Tokenize>(
|
||||||
&self,
|
&self,
|
||||||
function: &Function,
|
function: &Function,
|
||||||
args: &[ethabi::Token],
|
args: T,
|
||||||
) -> Result<Sender<'a, S, P>, ethabi::Error> {
|
) -> Result<Sender<'a, S, P>, abi::Error> {
|
||||||
// create the calldata
|
// create the calldata
|
||||||
let data = function.encode_input(args)?;
|
let data = function.encode_input(&args.into_tokens())?;
|
||||||
|
|
||||||
// create the tx object
|
// create the tx object
|
||||||
let tx = TransactionRequest {
|
let tx = TransactionRequest {
|
||||||
|
@ -99,9 +106,6 @@ impl<'a, S, P> Contract<'a, S, P> {
|
||||||
pub fn abi(&self) -> &Abi {
|
pub fn abi(&self) -> &Abi {
|
||||||
&self.abi
|
&self.abi
|
||||||
}
|
}
|
||||||
|
|
||||||
// call events
|
|
||||||
// deploy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Sender<'a, S, P> {
|
pub struct Sender<'a, S, P> {
|
||||||
|
@ -146,6 +150,68 @@ impl<'a, S: Signer, P: JsonRpcClient> Sender<'a, S, P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Event<'a, 'b, P> {
|
||||||
|
filter: Filter,
|
||||||
|
provider: &'a Provider<P>,
|
||||||
|
event: &'b abi::Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy of the builder pattern from Filter
|
||||||
|
impl<'a, 'b, P> Event<'a, 'b, P> {
|
||||||
|
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
|
||||||
|
self.filter.from_block = Some(block.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
|
||||||
|
self.filter.to_block = Some(block.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn topic<T: Into<ValueOrArray<H256>>>(mut self, topic: T) -> Self {
|
||||||
|
self.filter.topics.push(topic.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn topics(mut self, topics: &[ValueOrArray<H256>]) -> Self {
|
||||||
|
self.filter.topics.extend_from_slice(topics);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, P: JsonRpcClient> Event<'a, 'b, P> {
|
||||||
|
pub async fn query<T: Detokenize>(self) -> Result<Vec<T>, P::Error> {
|
||||||
|
// get the logs
|
||||||
|
let logs = self.provider.get_logs(&self.filter).await?;
|
||||||
|
|
||||||
|
let events = logs
|
||||||
|
.into_iter()
|
||||||
|
.map(|log| {
|
||||||
|
// ethabi parses the unindexed and indexed logs together to a
|
||||||
|
// vector of tokens
|
||||||
|
let tokens = self
|
||||||
|
.event
|
||||||
|
.parse_log(abi::RawLog {
|
||||||
|
topics: log.topics,
|
||||||
|
data: log.data.0,
|
||||||
|
})
|
||||||
|
.unwrap() // TODO: remove
|
||||||
|
.params
|
||||||
|
.into_iter()
|
||||||
|
.map(|param| param.value)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// convert the tokens to the requested datatype
|
||||||
|
T::from_tokens(tokens).unwrap()
|
||||||
|
})
|
||||||
|
.collect::<Vec<T>>();
|
||||||
|
|
||||||
|
Ok(events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
/// Utility function for creating a mapping between a unique signature and a
|
/// Utility function for creating a mapping between a unique signature and a
|
||||||
/// name-index pair for accessing contract ABI items.
|
/// name-index pair for accessing contract ABI items.
|
||||||
fn create_mapping<T, S, F>(
|
fn create_mapping<T, S, F>(
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
mod contract;
|
mod contract;
|
||||||
pub use contract::Contract;
|
pub use contract::Contract;
|
||||||
|
|
||||||
|
mod tokens;
|
||||||
|
pub use tokens::{Detokenize, InvalidOutputType, Tokenize};
|
||||||
|
|
|
@ -0,0 +1,528 @@
|
||||||
|
//! Contract Functions Output types.
|
||||||
|
//! Adapted from: https://github.com/tomusdrw/rust-web3/blob/master/src/contract/tokens.rs
|
||||||
|
|
||||||
|
use crate::types::{Address, Bytes, H256, U128, U256};
|
||||||
|
use arrayvec::ArrayVec;
|
||||||
|
use ethabi::Token;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error)]
|
||||||
|
#[error("{0}")]
|
||||||
|
pub struct InvalidOutputType(String);
|
||||||
|
|
||||||
|
/// Output type possible to deserialize from Contract ABI
|
||||||
|
pub trait Detokenize {
|
||||||
|
/// Creates a new instance from parsed ABI tokens.
|
||||||
|
fn from_tokens(tokens: Vec<Token>) -> Result<Self, InvalidOutputType>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Tokenizable> Detokenize for T {
|
||||||
|
fn from_tokens(mut tokens: Vec<Token>) -> Result<Self, InvalidOutputType> {
|
||||||
|
if tokens.len() != 1 {
|
||||||
|
Err(InvalidOutputType(format!(
|
||||||
|
"Expected single element, got a list: {:?}",
|
||||||
|
tokens
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Self::from_token(
|
||||||
|
tokens
|
||||||
|
.drain(..)
|
||||||
|
.next()
|
||||||
|
.expect("At least one element in vector; qed"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_output {
|
||||||
|
($num: expr, $( $ty: ident , )+) => {
|
||||||
|
impl<$($ty, )+> Detokenize for ($($ty,)+) where
|
||||||
|
$(
|
||||||
|
$ty: Tokenizable,
|
||||||
|
)+
|
||||||
|
{
|
||||||
|
fn from_tokens(mut tokens: Vec<Token>) -> Result<Self, InvalidOutputType> {
|
||||||
|
if tokens.len() != $num {
|
||||||
|
return Err(InvalidOutputType(format!(
|
||||||
|
"Expected {} elements, got a list of {}: {:?}",
|
||||||
|
$num,
|
||||||
|
tokens.len(),
|
||||||
|
tokens
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mut it = tokens.drain(..);
|
||||||
|
Ok(($(
|
||||||
|
$ty::from_token(it.next().expect("All elements are in vector; qed"))?,
|
||||||
|
)+))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_output!(1, A,);
|
||||||
|
impl_output!(2, A, B,);
|
||||||
|
impl_output!(3, A, B, C,);
|
||||||
|
impl_output!(4, A, B, C, D,);
|
||||||
|
// impl_output!(5, A, B, C, D, E,);
|
||||||
|
// impl_output!(6, A, B, C, D, E, F,);
|
||||||
|
// impl_output!(7, A, B, C, D, E, F, G,);
|
||||||
|
// impl_output!(8, A, B, C, D, E, F, G, H,);
|
||||||
|
// impl_output!(9, A, B, C, D, E, F, G, H, I,);
|
||||||
|
// impl_output!(10, A, B, C, D, E, F, G, H, I, J,);
|
||||||
|
// impl_output!(11, A, B, C, D, E, F, G, H, I, J, K,);
|
||||||
|
// impl_output!(12, A, B, C, D, E, F, G, H, I, J, K, L,);
|
||||||
|
// impl_output!(13, A, B, C, D, E, F, G, H, I, J, K, L, M,);
|
||||||
|
// impl_output!(14, A, B, C, D, E, F, G, H, I, J, K, L, M, N,);
|
||||||
|
// impl_output!(15, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O,);
|
||||||
|
// impl_output!(16, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P,);
|
||||||
|
|
||||||
|
/// Tokens conversion trait
|
||||||
|
pub trait Tokenize {
|
||||||
|
/// Convert to list of tokens
|
||||||
|
fn into_tokens(self) -> Vec<Token>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Tokenize for &'a [Token] {
|
||||||
|
fn into_tokens(self) -> Vec<Token> {
|
||||||
|
self.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Tokenizable> Tokenize for T {
|
||||||
|
fn into_tokens(self) -> Vec<Token> {
|
||||||
|
vec![self.into_token()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokenize for () {
|
||||||
|
fn into_tokens(self) -> Vec<Token> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_tokens {
|
||||||
|
($( $ty: ident : $no: tt, )+) => {
|
||||||
|
impl<$($ty, )+> Tokenize for ($($ty,)+) where
|
||||||
|
$(
|
||||||
|
$ty: Tokenizable,
|
||||||
|
)+
|
||||||
|
{
|
||||||
|
fn into_tokens(self) -> Vec<Token> {
|
||||||
|
vec![
|
||||||
|
$( self.$no.into_token(), )+
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_tokens!(A:0, );
|
||||||
|
impl_tokens!(A:0, B:1, );
|
||||||
|
impl_tokens!(A:0, B:1, C:2, );
|
||||||
|
impl_tokens!(A:0, B:1, C:2, D:3, );
|
||||||
|
|
||||||
|
// Commented out macros to reduce codegen time. Re-enable if needed.
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, );
|
||||||
|
// impl_tokens!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, );
|
||||||
|
|
||||||
|
/// Simplified output type for single value.
|
||||||
|
pub trait Tokenizable {
|
||||||
|
/// Converts a `Token` into expected type.
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
/// Converts a specified type back into token.
|
||||||
|
fn into_token(self) -> Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokenizable for Token {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokenizable for String {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::String(s) => Ok(s),
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `String`, got {:?}",
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::String(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokenizable for Bytes {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::Bytes(s) => Ok(s.into()),
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `Bytes`, got {:?}",
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::Bytes(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokenizable for H256 {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::FixedBytes(mut s) => {
|
||||||
|
if s.len() != 32 {
|
||||||
|
return Err(InvalidOutputType(format!("Expected `H256`, got {:?}", s)));
|
||||||
|
}
|
||||||
|
let mut data = [0; 32];
|
||||||
|
for (idx, val) in s.drain(..).enumerate() {
|
||||||
|
data[idx] = val;
|
||||||
|
}
|
||||||
|
Ok(data.into())
|
||||||
|
}
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `H256`, got {:?}",
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::FixedBytes(self.as_ref().to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokenizable for Address {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::Address(data) => Ok(data),
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `Address`, got {:?}",
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::Address(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! eth_uint_tokenizable {
|
||||||
|
($uint: ident, $name: expr) => {
|
||||||
|
impl Tokenizable for $uint {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::Int(data) | Token::Uint(data) => {
|
||||||
|
Ok(::std::convert::TryInto::try_into(data).unwrap())
|
||||||
|
}
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `{}`, got {:?}",
|
||||||
|
$name, other
|
||||||
|
))
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::Uint(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
eth_uint_tokenizable!(U256, "U256");
|
||||||
|
eth_uint_tokenizable!(U128, "U128");
|
||||||
|
|
||||||
|
macro_rules! int_tokenizable {
|
||||||
|
($int: ident, $token: ident) => {
|
||||||
|
impl Tokenizable for $int {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::Int(data) | Token::Uint(data) => Ok(data.low_u128() as _),
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `{}`, got {:?}",
|
||||||
|
stringify!($int),
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
// this should get optimized away by the compiler for unsigned integers
|
||||||
|
#[allow(unused_comparisons)]
|
||||||
|
let data = if self < 0 {
|
||||||
|
// NOTE: Rust does sign extension when converting from a
|
||||||
|
// signed integer to an unsigned integer, so:
|
||||||
|
// `-1u8 as u128 == u128::max_value()`
|
||||||
|
U256::from(self as u128) | U256([0, 0, u64::max_value(), u64::max_value()])
|
||||||
|
} else {
|
||||||
|
self.into()
|
||||||
|
};
|
||||||
|
Token::$token(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int_tokenizable!(i8, Int);
|
||||||
|
int_tokenizable!(i16, Int);
|
||||||
|
int_tokenizable!(i32, Int);
|
||||||
|
int_tokenizable!(i64, Int);
|
||||||
|
int_tokenizable!(i128, Int);
|
||||||
|
int_tokenizable!(u8, Uint);
|
||||||
|
int_tokenizable!(u16, Uint);
|
||||||
|
int_tokenizable!(u32, Uint);
|
||||||
|
int_tokenizable!(u64, Uint);
|
||||||
|
int_tokenizable!(u128, Uint);
|
||||||
|
|
||||||
|
impl Tokenizable for bool {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::Bool(data) => Ok(data),
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `bool`, got {:?}",
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::Bool(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker trait for `Tokenizable` types that are can tokenized to and from a
|
||||||
|
/// `Token::Array` and `Token:FixedArray`.
|
||||||
|
pub trait TokenizableItem: Tokenizable {}
|
||||||
|
|
||||||
|
macro_rules! tokenizable_item {
|
||||||
|
($($type: ty,)*) => {
|
||||||
|
$(
|
||||||
|
impl TokenizableItem for $type {}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenizable_item! {
|
||||||
|
Token, String, Address, H256, U256, U128, bool, Vec<u8>,
|
||||||
|
i8, i16, i32, i64, i128, u16, u32, u64, u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokenizable for Vec<u8> {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::Bytes(data) => Ok(data),
|
||||||
|
Token::FixedBytes(data) => Ok(data),
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `bytes`, got {:?}",
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::Bytes(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TokenizableItem> Tokenizable for Vec<T> {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::FixedArray(tokens) | Token::Array(tokens) => {
|
||||||
|
tokens.into_iter().map(Tokenizable::from_token).collect()
|
||||||
|
}
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `Array`, got {:?}",
|
||||||
|
other
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::Array(self.into_iter().map(Tokenizable::into_token).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TokenizableItem> TokenizableItem for Vec<T> {}
|
||||||
|
|
||||||
|
macro_rules! impl_fixed_types {
|
||||||
|
($num: expr) => {
|
||||||
|
impl Tokenizable for [u8; $num] {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::FixedBytes(bytes) => {
|
||||||
|
if bytes.len() != $num {
|
||||||
|
return Err(InvalidOutputType(format!(
|
||||||
|
"Expected `FixedBytes({})`, got FixedBytes({})",
|
||||||
|
$num,
|
||||||
|
bytes.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut arr = [0; $num];
|
||||||
|
arr.copy_from_slice(&bytes);
|
||||||
|
Ok(arr)
|
||||||
|
}
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `FixedBytes({})`, got {:?}",
|
||||||
|
$num, other
|
||||||
|
))
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::FixedBytes(self.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenizableItem for [u8; $num] {}
|
||||||
|
|
||||||
|
impl<T: TokenizableItem + Clone> Tokenizable for [T; $num] {
|
||||||
|
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
|
||||||
|
match token {
|
||||||
|
Token::FixedArray(tokens) => {
|
||||||
|
if tokens.len() != $num {
|
||||||
|
return Err(InvalidOutputType(format!(
|
||||||
|
"Expected `FixedArray({})`, got FixedArray({})",
|
||||||
|
$num,
|
||||||
|
tokens.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut arr = ArrayVec::<[T; $num]>::new();
|
||||||
|
let mut it = tokens.into_iter().map(T::from_token);
|
||||||
|
for _ in 0..$num {
|
||||||
|
arr.push(it.next().expect("Length validated in guard; qed")?);
|
||||||
|
}
|
||||||
|
// Can't use expect here because [T; $num]: Debug is not satisfied.
|
||||||
|
match arr.into_inner() {
|
||||||
|
Ok(arr) => Ok(arr),
|
||||||
|
Err(_) => panic!("All elements inserted so the array is full; qed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => Err(InvalidOutputType(format!(
|
||||||
|
"Expected `FixedArray({})`, got {:?}",
|
||||||
|
$num, other
|
||||||
|
))
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token(self) -> Token {
|
||||||
|
Token::FixedArray(
|
||||||
|
ArrayVec::from(self)
|
||||||
|
.into_iter()
|
||||||
|
.map(T::into_token)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TokenizableItem + Clone> TokenizableItem for [T; $num] {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_fixed_types!(1);
|
||||||
|
impl_fixed_types!(2);
|
||||||
|
impl_fixed_types!(3);
|
||||||
|
impl_fixed_types!(4);
|
||||||
|
impl_fixed_types!(5);
|
||||||
|
impl_fixed_types!(6);
|
||||||
|
impl_fixed_types!(7);
|
||||||
|
impl_fixed_types!(8);
|
||||||
|
impl_fixed_types!(9);
|
||||||
|
impl_fixed_types!(10);
|
||||||
|
impl_fixed_types!(11);
|
||||||
|
impl_fixed_types!(12);
|
||||||
|
impl_fixed_types!(13);
|
||||||
|
impl_fixed_types!(14);
|
||||||
|
impl_fixed_types!(15);
|
||||||
|
impl_fixed_types!(16);
|
||||||
|
impl_fixed_types!(32);
|
||||||
|
impl_fixed_types!(64);
|
||||||
|
impl_fixed_types!(128);
|
||||||
|
impl_fixed_types!(256);
|
||||||
|
impl_fixed_types!(512);
|
||||||
|
impl_fixed_types!(1024);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Detokenize, Tokenizable};
|
||||||
|
use crate::types::{Address, U256};
|
||||||
|
use ethabi::Token;
|
||||||
|
|
||||||
|
fn output<R: Detokenize>() -> R {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn should_be_able_to_compile() {
|
||||||
|
let _tokens: Vec<Token> = output();
|
||||||
|
let _uint: U256 = output();
|
||||||
|
let _address: Address = output();
|
||||||
|
let _string: String = output();
|
||||||
|
let _bool: bool = output();
|
||||||
|
let _bytes: Vec<u8> = output();
|
||||||
|
|
||||||
|
let _pair: (U256, bool) = output();
|
||||||
|
let _vec: Vec<U256> = output();
|
||||||
|
let _array: [U256; 4] = output();
|
||||||
|
let _bytes: Vec<[[u8; 1]; 64]> = output();
|
||||||
|
|
||||||
|
let _mixed: (Vec<Vec<u8>>, [U256; 4], Vec<U256>, U256) = output();
|
||||||
|
|
||||||
|
let _ints: (i16, i32, i64, i128) = output();
|
||||||
|
let _uints: (u16, u32, u64, u128) = output();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_decode_array_of_fixed_bytes() {
|
||||||
|
// byte[8][]
|
||||||
|
let tokens = vec![Token::FixedArray(vec![
|
||||||
|
Token::FixedBytes(vec![1]),
|
||||||
|
Token::FixedBytes(vec![2]),
|
||||||
|
Token::FixedBytes(vec![3]),
|
||||||
|
Token::FixedBytes(vec![4]),
|
||||||
|
Token::FixedBytes(vec![5]),
|
||||||
|
Token::FixedBytes(vec![6]),
|
||||||
|
Token::FixedBytes(vec![7]),
|
||||||
|
Token::FixedBytes(vec![8]),
|
||||||
|
])];
|
||||||
|
let data: [[u8; 1]; 8] = Detokenize::from_tokens(tokens).unwrap();
|
||||||
|
assert_eq!(data[0][0], 1);
|
||||||
|
assert_eq!(data[1][0], 2);
|
||||||
|
assert_eq!(data[2][0], 3);
|
||||||
|
assert_eq!(data[7][0], 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_sign_extend_negative_integers() {
|
||||||
|
assert_eq!((-1i8).into_token(), Token::Int(U256::MAX));
|
||||||
|
assert_eq!((-2i16).into_token(), Token::Int(U256::MAX - 1));
|
||||||
|
assert_eq!((-3i32).into_token(), Token::Int(U256::MAX - 2));
|
||||||
|
assert_eq!((-4i64).into_token(), Token::Int(U256::MAX - 3));
|
||||||
|
assert_eq!((-5i128).into_token(), Token::Int(U256::MAX - 4));
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,11 +67,11 @@ pub struct Log {
|
||||||
pub struct Filter {
|
pub struct Filter {
|
||||||
/// From Block
|
/// From Block
|
||||||
#[serde(rename = "fromBlock", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "fromBlock", skip_serializing_if = "Option::is_none")]
|
||||||
from_block: Option<BlockNumber>,
|
pub(crate) from_block: Option<BlockNumber>,
|
||||||
|
|
||||||
/// To Block
|
/// To Block
|
||||||
#[serde(rename = "toBlock", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "toBlock", skip_serializing_if = "Option::is_none")]
|
||||||
to_block: Option<BlockNumber>,
|
pub(crate) to_block: Option<BlockNumber>,
|
||||||
|
|
||||||
/// Address
|
/// Address
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
@ -82,7 +82,7 @@ pub struct Filter {
|
||||||
/// Topics
|
/// Topics
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
// TODO: Split in an event name + 3 topics
|
// TODO: Split in an event name + 3 topics
|
||||||
topics: Vec<ValueOrArray<H256>>,
|
pub(crate) topics: Vec<ValueOrArray<H256>>,
|
||||||
|
|
||||||
/// Limit
|
/// Limit
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
|
@ -4,7 +4,7 @@ pub type Selector = [u8; 4];
|
||||||
|
|
||||||
// Re-export common ethereum datatypes with more specific names
|
// Re-export common ethereum datatypes with more specific names
|
||||||
pub use ethereum_types::H256 as TxHash;
|
pub use ethereum_types::H256 as TxHash;
|
||||||
pub use ethereum_types::{Address, Bloom, H256, U256, U64};
|
pub use ethereum_types::{Address, Bloom, H256, U128, U256, U64};
|
||||||
|
|
||||||
mod transaction;
|
mod transaction;
|
||||||
pub use transaction::{Overrides, Transaction, TransactionReceipt, TransactionRequest};
|
pub use transaction::{Overrides, Transaction, TransactionReceipt, TransactionRequest};
|
||||||
|
@ -22,4 +22,4 @@ mod block;
|
||||||
pub use block::{Block, BlockId, BlockNumber};
|
pub use block::{Block, BlockId, BlockNumber};
|
||||||
|
|
||||||
mod log;
|
mod log;
|
||||||
pub use log::{Filter, Log};
|
pub use log::{Filter, Log, ValueOrArray};
|
||||||
|
|
Loading…
Reference in New Issue