Futures-based native wrappers for contract ABIs (#5341)
* initial native contract generator * get generated code compiling * unit tests for type codegen * autogenerate registry contract * native_contracts entry for registry * service_transaction_checker * fixed indentation
This commit is contained in:
committed by
Gav Wood
parent
21e21f1e02
commit
c9c8f920d2
346
ethcore/native_contracts/generator/src/lib.rs
Normal file
346
ethcore/native_contracts/generator/src/lib.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Rust code contract generator.
|
||||
//! The code generated will require a dependence on the `ethcore-util`,
|
||||
//! `ethabi`, `byteorder`, and `futures` crates.
|
||||
//! This currently isn't hygienic, so compilation of generated code may fail
|
||||
//! due to missing crates or name collisions. This will change when
|
||||
//! it can be ported to a procedural macro.
|
||||
|
||||
use ethabi::Contract;
|
||||
use ethabi::spec::{Interface, ParamType, Error as AbiError};
|
||||
use heck::SnakeCase;
|
||||
|
||||
extern crate ethabi;
|
||||
extern crate heck;
|
||||
|
||||
/// Errors in generation.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Bad ABI.
|
||||
Abi(AbiError),
|
||||
/// Unsupported parameter type in given function.
|
||||
UnsupportedType(String, ParamType),
|
||||
}
|
||||
|
||||
/// Given an ABI string, generate code for a a Rust module containing
|
||||
/// a struct which can be used to call it.
|
||||
// TODO: make this a proc macro when that's possible.
|
||||
pub fn generate_module(struct_name: &str, abi: &str) -> Result<String, Error> {
|
||||
let contract = Contract::new(Interface::load(abi.as_bytes()).map_err(Error::Abi)?);
|
||||
let functions = generate_functions(&contract)?;
|
||||
|
||||
Ok(format!(r##"
|
||||
use byteorder::{{BigEndian, ByteOrder}};
|
||||
use futures::{{future, Future, BoxFuture}};
|
||||
use ethabi::{{Contract, Interface, Token}};
|
||||
use util::{{self, Uint}};
|
||||
|
||||
pub struct {name} {{
|
||||
contract: Contract,
|
||||
/// Address to make calls to.
|
||||
pub address: util::Address,
|
||||
}}
|
||||
|
||||
const ABI: &'static str = r#"{abi_str}"#;
|
||||
|
||||
impl {name} {{
|
||||
/// Create a new instance of `{name}` with an address.
|
||||
/// Calls can be made, given a callback for dispatching calls asynchronously.
|
||||
pub fn new(address: util::Address) -> Self {{
|
||||
let contract = Contract::new(Interface::load(ABI.as_bytes())
|
||||
.expect("ABI checked at generation-time; qed"));
|
||||
{name} {{
|
||||
contract: contract,
|
||||
address: address,
|
||||
}}
|
||||
}}
|
||||
|
||||
{functions}
|
||||
}}
|
||||
"##,
|
||||
name = struct_name,
|
||||
abi_str = abi,
|
||||
functions = functions,
|
||||
))
|
||||
}
|
||||
|
||||
// generate function bodies from the ABI.
|
||||
fn generate_functions(contract: &Contract) -> Result<String, Error> {
|
||||
let mut functions = String::new();
|
||||
for function in contract.functions() {
|
||||
let name = function.name();
|
||||
let snake_name = name.to_snake_case();
|
||||
let inputs = function.input_params();
|
||||
let outputs = function.output_params();
|
||||
|
||||
let (input_params, to_tokens) = input_params_codegen(&inputs)
|
||||
.map_err(|bad_type| Error::UnsupportedType(name.into(), bad_type))?;
|
||||
|
||||
let (output_type, decode_outputs) = output_params_codegen(&outputs)
|
||||
.map_err(|bad_type| Error::UnsupportedType(name.into(), bad_type))?;
|
||||
|
||||
functions.push_str(&format!(r##"
|
||||
/// Call the function "{abi_name}" on the contract.
|
||||
/// Inputs: {abi_inputs:?}
|
||||
/// Outputs: {abi_outputs:?}
|
||||
pub fn {snake_name}<F, U>(&self, call: F, {params}) -> BoxFuture<{output_type}, String>
|
||||
where F: Fn(util::Address, Vec<u8>) -> U, U: Future<Item=Vec<u8>, Error=String> + Send + 'static
|
||||
{{
|
||||
let function = self.contract.function(r#"{abi_name}"#.to_string())
|
||||
.expect("function existence checked at compile-time; qed");
|
||||
let call_addr = self.address;
|
||||
|
||||
let call_future = match function.encode_call({to_tokens}) {{
|
||||
Ok(call_data) => (call)(call_addr, call_data),
|
||||
Err(e) => return future::err(format!("Error encoding call: {{:?}}", e)).boxed(),
|
||||
}};
|
||||
|
||||
call_future
|
||||
.and_then(move |out| function.decode_output(out).map_err(|e| format!("{{:?}}", e)))
|
||||
.map(::std::collections::VecDeque::from)
|
||||
.and_then(|mut outputs| {decode_outputs})
|
||||
.boxed()
|
||||
}}
|
||||
"##,
|
||||
abi_name = name,
|
||||
abi_inputs = inputs,
|
||||
abi_outputs = outputs,
|
||||
snake_name = snake_name,
|
||||
params = input_params,
|
||||
output_type = output_type,
|
||||
to_tokens = to_tokens,
|
||||
decode_outputs = decode_outputs,
|
||||
))
|
||||
}
|
||||
|
||||
Ok(functions)
|
||||
}
|
||||
|
||||
// generate code for params in function signature and turning them into tokens.
|
||||
//
|
||||
// two pieces of code are generated: the first gives input types for the function signature,
|
||||
// and the second gives code to tokenize those inputs.
|
||||
//
|
||||
// params of form `param_0: type_0, param_1: type_1, ...`
|
||||
// tokenizing code of form `{let mut tokens = Vec::new(); tokens.push({param_X}); tokens }`
|
||||
//
|
||||
// returns any unsupported param type encountered.
|
||||
fn input_params_codegen(inputs: &[ParamType]) -> Result<(String, String), ParamType> {
|
||||
let mut params = String::new();
|
||||
let mut to_tokens = "{ let mut tokens = Vec::new();".to_string();
|
||||
|
||||
for (index, param_type) in inputs.iter().enumerate() {
|
||||
let param_name = format!("param_{}", index);
|
||||
let rust_type = rust_type(param_type.clone())?;
|
||||
let (needs_mut, tokenize_code) = tokenize(¶m_name, param_type.clone());
|
||||
|
||||
params.push_str(&format!("{}{}: {}, ",
|
||||
if needs_mut { "mut " } else { "" }, param_name, rust_type));
|
||||
|
||||
to_tokens.push_str(&format!("tokens.push({{ {} }});", tokenize_code));
|
||||
}
|
||||
|
||||
to_tokens.push_str(" tokens }");
|
||||
Ok((params, to_tokens))
|
||||
}
|
||||
|
||||
// generate code for outputs of the function and detokenizing them.
|
||||
//
|
||||
// two pieces of code are generated: the first gives an output type for the function signature
|
||||
// as a tuple, and the second gives code to get that tuple from a deque of tokens.
|
||||
//
|
||||
// produce output type of the form (type_1, type_2, ...) without trailing comma.
|
||||
// produce code for getting this output type from `outputs: VecDeque<Token>`, where
|
||||
// an `Err(String)` can be returned.
|
||||
//
|
||||
// returns any unsupported param type encountered.
|
||||
fn output_params_codegen(outputs: &[ParamType]) -> Result<(String, String), ParamType> {
|
||||
let mut output_type = "(".to_string();
|
||||
let mut decode_outputs = "Ok((".to_string();
|
||||
|
||||
for (index, output) in outputs.iter().cloned().enumerate() {
|
||||
let rust_type = rust_type(output.clone())?;
|
||||
|
||||
output_type.push_str(&rust_type);
|
||||
|
||||
decode_outputs.push_str(&format!(
|
||||
r#"
|
||||
outputs
|
||||
.pop_front()
|
||||
.and_then(|output| {{ {} }})
|
||||
.ok_or_else(|| "Wrong output type".to_string())?
|
||||
"#,
|
||||
detokenize("output", output)
|
||||
));
|
||||
|
||||
// don't append trailing commas for the last element
|
||||
// so we can reuse the same code for single-output contracts,
|
||||
// since T == (T) != (T,)
|
||||
if index < outputs.len() - 1 {
|
||||
output_type.push_str(", ");
|
||||
decode_outputs.push_str(", ");
|
||||
}
|
||||
}
|
||||
|
||||
output_type.push_str(")");
|
||||
decode_outputs.push_str("))");
|
||||
Ok((output_type, decode_outputs))
|
||||
}
|
||||
|
||||
// create code for an argument type from param type.
|
||||
fn rust_type(input: ParamType) -> Result<String, ParamType> {
|
||||
Ok(match input {
|
||||
ParamType::Address => "util::Address".into(),
|
||||
ParamType::FixedBytes(len) if len <= 32 => format!("util::H{}", len * 8),
|
||||
ParamType::Bytes | ParamType::FixedBytes(_) => "Vec<u8>".into(),
|
||||
ParamType::Int(width) => match width {
|
||||
8 | 16 | 32 | 64 => format!("i{}", width),
|
||||
_ => return Err(ParamType::Int(width)),
|
||||
},
|
||||
ParamType::Uint(width) => match width {
|
||||
8 | 16 | 32 | 64 => format!("u{}", width),
|
||||
128 | 160 | 256 => format!("util::U{}", width),
|
||||
_ => return Err(ParamType::Uint(width)),
|
||||
},
|
||||
ParamType::Bool => "bool".into(),
|
||||
ParamType::String => "String".into(),
|
||||
ParamType::Array(kind) => format!("Vec<{}>", rust_type(*kind)?),
|
||||
other => return Err(other),
|
||||
})
|
||||
}
|
||||
|
||||
// create code for tokenizing this parameter.
|
||||
// returns (needs_mut, code), where needs_mut indicates mutability required.
|
||||
// panics on unsupported types.
|
||||
fn tokenize(name: &str, input: ParamType) -> (bool, String) {
|
||||
let mut needs_mut = false;
|
||||
let code = match input {
|
||||
ParamType::Address => format!("Token::Address({}.0)", name),
|
||||
ParamType::Bytes => format!("Token::Bytes({})", name),
|
||||
ParamType::FixedBytes(len) if len <= 32 =>
|
||||
format!("Token::FixedBytes({}.0.to_vec())", name),
|
||||
ParamType::FixedBytes(len) => {
|
||||
needs_mut = true;
|
||||
format!("{}.resize({}, 0); Token::FixedBytes({})", name, len, name)
|
||||
}
|
||||
ParamType::Int(width) => match width {
|
||||
8 => format!("let mut r = [0xff; 32]; r[31] = {}; Token::Int(r)", name),
|
||||
16 | 32 | 64 =>
|
||||
format!("let mut r = [0xff; 32]; BigEndian::write_i{}(&mut r[{}..], {}); Token::Int(r))",
|
||||
width, 32 - (width / 8), name),
|
||||
_ => panic!("Signed int with more than 64 bits not supported."),
|
||||
},
|
||||
ParamType::Uint(width) => format!(
|
||||
"let mut r = [0; 32]; {}.to_big_endian(&mut r); Token::Uint(r)",
|
||||
if width <= 64 { format!("util::U256::from({} as u64)", name) }
|
||||
else { format!("util::U256::from({})", name) }
|
||||
),
|
||||
ParamType::Bool => format!("Token::Bool({})", name),
|
||||
ParamType::String => format!("Token::String({})", name),
|
||||
ParamType::Array(kind) => {
|
||||
let (needs_mut, code) = tokenize("i", *kind);
|
||||
format!("Token::Array({}.into_iter().map(|{}i| {{ {} }}).collect())",
|
||||
name, if needs_mut { "mut " } else { "" }, code)
|
||||
}
|
||||
ParamType::FixedArray(_, _) => panic!("Fixed-length arrays not supported."),
|
||||
};
|
||||
|
||||
(needs_mut, code)
|
||||
}
|
||||
|
||||
// create code for detokenizing this parameter.
|
||||
// takes an output type and the identifier of a token.
|
||||
// expands to code that evaluates to a Option<concrete type>
|
||||
// panics on unsupported types.
|
||||
fn detokenize(name: &str, output_type: ParamType) -> String {
|
||||
match output_type {
|
||||
ParamType::Address => format!("{}.to_address().map(util::H160)", name),
|
||||
ParamType::Bytes => format!("{}.to_bytes()", name),
|
||||
ParamType::FixedBytes(len) if len <= 32 => {
|
||||
// ensure no panic on slice too small.
|
||||
let read_hash = format!("b.resize({}, 0); util::H{}::from_slice(&b[..{}])",
|
||||
len, len * 8, len);
|
||||
|
||||
format!("{}.to_fixed_bytes().map(|mut b| {{ {} }})",
|
||||
name, read_hash)
|
||||
}
|
||||
ParamType::FixedBytes(_) => format!("{}.to_fixed_bytes()", name),
|
||||
ParamType::Int(width) => {
|
||||
let read_int = match width {
|
||||
8 => "i[31] as i8".into(),
|
||||
16 | 32 | 64 => format!("BigEndian::read_i{}(&i[{}..])", width, 32 - (width / 8)),
|
||||
_ => panic!("Signed integers over 64 bytes not allowed."),
|
||||
};
|
||||
format!("{}.to_int().map(|i| {})", name, read_int)
|
||||
}
|
||||
ParamType::Uint(width) => {
|
||||
let read_uint = match width {
|
||||
8 | 16 | 32 | 64 => format!("util::U256(u).low_u64() as u{}", width),
|
||||
_ => format!("util::U{}::from(&u[..])", width),
|
||||
};
|
||||
|
||||
format!("{}.to_uint().map(|u| {})", name, read_uint)
|
||||
}
|
||||
ParamType::Bool => format!("{}.to_bool()", name),
|
||||
ParamType::String => format!("{}.to_string()", name),
|
||||
ParamType::Array(kind) => {
|
||||
let read_array = format!("x.into_iter().map(|a| {{ {} }}).collect::<Option<Vec<_>>()",
|
||||
detokenize("a", *kind));
|
||||
|
||||
format!("{}.to_array().and_then(|x| {})",
|
||||
name, read_array)
|
||||
}
|
||||
ParamType::FixedArray(_, _) => panic!("Fixed-length arrays not supported.")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ethabi::spec::ParamType;
|
||||
|
||||
#[test]
|
||||
fn input_types() {
|
||||
assert_eq!(::input_params_codegen(&[]).unwrap().0, "");
|
||||
assert_eq!(::input_params_codegen(&[ParamType::Address]).unwrap().0, "param_0: util::Address, ");
|
||||
assert_eq!(::input_params_codegen(&[ParamType::Address, ParamType::Bytes]).unwrap().0,
|
||||
"param_0: util::Address, param_1: Vec<u8>, ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_types() {
|
||||
assert_eq!(::output_params_codegen(&[]).unwrap().0, "()");
|
||||
assert_eq!(::output_params_codegen(&[ParamType::Address]).unwrap().0, "(util::Address)");
|
||||
assert_eq!(::output_params_codegen(&[ParamType::Address, ParamType::Array(Box::new(ParamType::Bytes))]).unwrap().0,
|
||||
"(util::Address, Vec<Vec<u8>>)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_type() {
|
||||
assert_eq!(::rust_type(ParamType::FixedBytes(32)).unwrap(), "util::H256");
|
||||
assert_eq!(::rust_type(ParamType::Array(Box::new(ParamType::FixedBytes(32)))).unwrap(),
|
||||
"Vec<util::H256>");
|
||||
|
||||
assert_eq!(::rust_type(ParamType::Uint(64)).unwrap(), "u64");
|
||||
assert!(::rust_type(ParamType::Uint(63)).is_err());
|
||||
|
||||
assert_eq!(::rust_type(ParamType::Int(32)).unwrap(), "i32");
|
||||
assert_eq!(::rust_type(ParamType::Uint(256)).unwrap(), "util::U256");
|
||||
}
|
||||
|
||||
// codegen tests will need bootstrapping of some kind.
|
||||
}
|
||||
Reference in New Issue
Block a user