Backport Beta 2.2.1 (#9905)
* Bump beta to version 2.2.1 * fix: Intermittent failing CI due to addr in use (#9885) Allow OS to set port at runtime * Use Weak reference in PubSubClient (#9886) * Fix json tracer overflow (#9873) * Fix json tracer overflow * Replace trace_executed with a direct trace push * Remove unused variable * Add test for 5a51 * Remove duplicate json! * Fix docker script (#9854) * Dockerfile: change source path of the newly added check_sync.sh (#9869) * Allow to seal work on latest block (#9876) * Allow to seal work on latest block. * Test from @todr to check sealing conditions. * gitlab-ci: make android release build succeed (#9743) * use docker cargo config file for android builds * make android build succeed * ethcore: use Machine::verify_transaction on parent block (#9900) * ethcore: use Machine::verify_transaction on parent block also fixes off-by-one activation of transaction permission contract * ethcore: clarify call to verify_transaction * foundation: #6692865, ropsten: #4417537, kovan: #9363457 * Remove rust-toolchain file (#9906) * EIP-712 implementation (#9631) * EIP-712 impl * added more tests * removed size parsing unwrap * corrected TYPE_REGEX to disallow zero sized fixed length arrays, replaced LinkedHashSet with IndexSet, added API spec to docs, fixed Type::Byte encoding branch * use Option<u64> instead of u64 for Type::Array::Length * replace `.iter()` with `.values()` Co-Authored-By: seunlanlege <seunlanlege@gmail.com> * tabify eip712.rs * use proper comments for docs * Cargo.lock: revert unrelated changes * tabify encode.rs * EIP 191 (#9701) * added sign_191 rpc method * fixed hash_structured_data return type * added ConfirmationPayload::SignMessage for non-prefixed signatures, added tests for sign191 * renamed WithValidator -> PresignedTransaction * rename applicationData to data in test * adds docs for EIP191Version, renamed SignRequest to EIP191SignRequest * light-fetch: Differentiate between out-of-gas/manual throw and use required gas from response on failure (#9824) * fix start_gas, handle OOG exceptions & NotEnoughGas * Change START_GAS: 50_000 -> 60_000 * When the `OutOfGas exception` is received then try to double the gas until it succeeds or block gas limit is reached * When `NotEnoughBasGas error` is received then use the required gas provided in the response * fix(light-fetch): ensure block_gas_limit is tried Try the `block_gas_limit` before regard the execution as an error * Update rpc/src/v1/helpers/light_fetch.rs Co-Authored-By: niklasad1 <niklasadolfsson1@gmail.com> * simplify cargo audit * Use block header for building finality (#9914) * ci: nuke the gitlab caches (#9855)
This commit is contained in:
23
util/EIP-712/Cargo.toml
Normal file
23
util/EIP-712/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "eip712"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
|
||||
[dependencies]
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
ethabi = "6.0"
|
||||
keccak-hash = "0.1"
|
||||
ethereum-types = "0.4"
|
||||
failure = "0.1"
|
||||
itertools = "0.7"
|
||||
failure_derive = "0.1"
|
||||
lazy_static = "1.1"
|
||||
toolshed = "0.4"
|
||||
regex = "1.0"
|
||||
validator = "0.8"
|
||||
validator_derive = "0.8"
|
||||
lunarity-lexer = "0.1"
|
||||
rustc-hex = "2.0"
|
||||
indexmap = "1.0.2"
|
||||
177
util/EIP-712/src/eip712.rs
Normal file
177
util/EIP-712/src/eip712.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright 2015-2018 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/>.
|
||||
|
||||
//! EIP712 structs
|
||||
use serde_json::{Value};
|
||||
use std::collections::HashMap;
|
||||
use ethereum_types::{U256, H256, Address};
|
||||
use regex::Regex;
|
||||
use validator::Validate;
|
||||
use validator::ValidationErrors;
|
||||
|
||||
pub(crate) type MessageTypes = HashMap<String, Vec<FieldType>>;
|
||||
|
||||
lazy_static! {
|
||||
// match solidity identifier with the addition of '[(\d)*]*'
|
||||
static ref TYPE_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9]*(\[([1-9]\d*)*\])*$").unwrap();
|
||||
static ref IDENT_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9]*$").unwrap();
|
||||
}
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[derive(Deserialize, Serialize, Validate, Debug, Clone)]
|
||||
pub(crate) struct EIP712Domain {
|
||||
pub(crate) name: String,
|
||||
pub(crate) version: String,
|
||||
pub(crate) chain_id: U256,
|
||||
pub(crate) verifying_contract: Address,
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub(crate) salt: Option<H256>,
|
||||
}
|
||||
/// EIP-712 struct
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct EIP712 {
|
||||
pub(crate) types: MessageTypes,
|
||||
pub(crate) primary_type: String,
|
||||
pub(crate) message: Value,
|
||||
pub(crate) domain: EIP712Domain,
|
||||
}
|
||||
|
||||
impl Validate for EIP712 {
|
||||
fn validate(&self) -> Result<(), ValidationErrors> {
|
||||
for field_types in self.types.values() {
|
||||
for field_type in field_types {
|
||||
field_type.validate()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Debug, Clone)]
|
||||
pub(crate) struct FieldType {
|
||||
#[validate(regex = "IDENT_REGEX")]
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
#[validate(regex = "TYPE_REGEX")]
|
||||
pub type_: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::from_str;
|
||||
|
||||
#[test]
|
||||
fn test_regex() {
|
||||
let test_cases = vec!["unint bytes32", "Seun\\[]", "byte[]uint", "byte[7[]uint][]", "Person[0]"];
|
||||
for case in test_cases {
|
||||
assert_eq!(TYPE_REGEX.is_match(case), false)
|
||||
}
|
||||
|
||||
let test_cases = vec!["bytes32", "Foo[]", "bytes1", "bytes32[][]", "byte[9]", "contents"];
|
||||
for case in test_cases {
|
||||
assert_eq!(TYPE_REGEX.is_match(case), true)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialization() {
|
||||
let string = r#"{
|
||||
"primaryType": "Mail",
|
||||
"domain": {
|
||||
"name": "Ether Mail",
|
||||
"version": "1",
|
||||
"chainId": "0x1",
|
||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
||||
},
|
||||
"message": {
|
||||
"from": {
|
||||
"name": "Cow",
|
||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||
},
|
||||
"to": {
|
||||
"name": "Bob",
|
||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||
},
|
||||
"contents": "Hello, Bob!"
|
||||
},
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "uint256" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Person": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "wallet", "type": "address" }
|
||||
],
|
||||
"Mail": [
|
||||
{ "name": "from", "type": "Person" },
|
||||
{ "name": "to", "type": "Person" },
|
||||
{ "name": "contents", "type": "string" }
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
let _ = from_str::<EIP712>(string).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_failing_deserialization() {
|
||||
let string = r#"{
|
||||
"primaryType": "Mail",
|
||||
"domain": {
|
||||
"name": "Ether Mail",
|
||||
"version": "1",
|
||||
"chainId": "0x1",
|
||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
||||
},
|
||||
"message": {
|
||||
"from": {
|
||||
"name": "Cow",
|
||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||
},
|
||||
"to": {
|
||||
"name": "Bob",
|
||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||
},
|
||||
"contents": "Hello, Bob!"
|
||||
},
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "7uint256[x] Seun" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Person": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "wallet amen", "type": "address" }
|
||||
],
|
||||
"Mail": [
|
||||
{ "name": "from", "type": "Person" },
|
||||
{ "name": "to", "type": "Person" },
|
||||
{ "name": "contents", "type": "string" }
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
let data = from_str::<EIP712>(string).unwrap();
|
||||
assert_eq!(data.validate().is_err(), true);
|
||||
}
|
||||
}
|
||||
415
util/EIP-712/src/encode.rs
Normal file
415
util/EIP-712/src/encode.rs
Normal file
@@ -0,0 +1,415 @@
|
||||
// Copyright 2015-2018 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/>.
|
||||
|
||||
//! EIP712 Encoder
|
||||
use ethabi::{encode, Token as EthAbiToken};
|
||||
use ethereum_types::{Address as EthAddress, U256, H256};
|
||||
use keccak_hash::keccak;
|
||||
use serde_json::Value;
|
||||
use std::str::FromStr;
|
||||
use itertools::Itertools;
|
||||
use indexmap::IndexSet;
|
||||
use serde_json::to_value;
|
||||
use parser::{Parser, Type};
|
||||
use error::{Result, ErrorKind, serde_error};
|
||||
use eip712::{EIP712, MessageTypes};
|
||||
use rustc_hex::FromHex;
|
||||
use validator::Validate;
|
||||
use std::collections::HashSet;
|
||||
|
||||
|
||||
fn check_hex(string: &str) -> Result<()> {
|
||||
if string.len() >= 2 && &string[..2] == "0x" {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
return Err(ErrorKind::HexParseError(
|
||||
format!("Expected a 0x-prefixed string of even length, found {} length string", string.len()))
|
||||
)?
|
||||
}
|
||||
/// given a type and HashMap<String, Vec<FieldType>>
|
||||
/// returns a HashSet of dependent types of the given type
|
||||
fn build_dependencies<'a>(message_type: &'a str, message_types: &'a MessageTypes) -> Option<(HashSet<&'a str>)>
|
||||
{
|
||||
if message_types.get(message_type).is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut types = IndexSet::new();
|
||||
types.insert(message_type);
|
||||
let mut deps = HashSet::new();
|
||||
|
||||
while let Some(item) = types.pop() {
|
||||
if let Some(fields) = message_types.get(item) {
|
||||
deps.insert(item);
|
||||
|
||||
for field in fields {
|
||||
// seen this type before? or not a custom type skip
|
||||
if deps.contains(&*field.type_) || !message_types.contains_key(&*field.type_) {
|
||||
continue;
|
||||
}
|
||||
types.insert(&*field.type_);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Some(deps)
|
||||
}
|
||||
|
||||
fn encode_type(message_type: &str, message_types: &MessageTypes) -> Result<String> {
|
||||
let deps = {
|
||||
let mut temp = build_dependencies(message_type, message_types).ok_or_else(|| ErrorKind::NonExistentType)?;
|
||||
temp.remove(message_type);
|
||||
let mut temp = temp.into_iter().collect::<Vec<_>>();
|
||||
(&mut temp[..]).sort_unstable();
|
||||
temp.insert(0, message_type);
|
||||
temp
|
||||
};
|
||||
|
||||
let encoded = deps
|
||||
.into_iter()
|
||||
.filter_map(|dep| {
|
||||
message_types.get(dep).map(|field_types| {
|
||||
let types = field_types
|
||||
.iter()
|
||||
.map(|value| format!("{} {}", value.type_, value.name))
|
||||
.join(",");
|
||||
return format!("{}({})", dep, types);
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.concat();
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
fn type_hash(message_type: &str, typed_data: &MessageTypes) -> Result<H256> {
|
||||
Ok(keccak(encode_type(message_type, typed_data)?))
|
||||
}
|
||||
|
||||
fn encode_data(
|
||||
parser: &Parser,
|
||||
message_type: &Type,
|
||||
message_types: &MessageTypes,
|
||||
value: &Value,
|
||||
field_name: Option<&str>
|
||||
) -> Result<Vec<u8>>
|
||||
{
|
||||
let encoded = match message_type {
|
||||
Type::Array {
|
||||
inner,
|
||||
length
|
||||
} => {
|
||||
let mut items = vec![];
|
||||
let values = value.as_array().ok_or_else(|| serde_error("array", field_name))?;
|
||||
|
||||
// check if the type definition actually matches
|
||||
// the length of items to be encoded
|
||||
if length.is_some() && Some(values.len() as u64) != *length {
|
||||
let array_type = format!("{}[{}]", *inner, length.unwrap());
|
||||
return Err(ErrorKind::UnequalArrayItems(length.unwrap(), array_type, values.len() as u64))?
|
||||
}
|
||||
|
||||
for item in values {
|
||||
let mut encoded = encode_data(parser, &*inner, &message_types, item, field_name)?;
|
||||
items.append(&mut encoded);
|
||||
}
|
||||
|
||||
keccak(items).to_vec()
|
||||
}
|
||||
|
||||
Type::Custom(ref ident) if message_types.get(&*ident).is_some() => {
|
||||
let type_hash = (&type_hash(ident, &message_types)?).to_vec();
|
||||
let mut tokens = encode(&[EthAbiToken::FixedBytes(type_hash)]);
|
||||
|
||||
for field in message_types.get(ident).expect("Already checked in match guard; qed") {
|
||||
let value = &value[&field.name];
|
||||
let type_ = parser.parse_type(&*field.type_)?;
|
||||
let mut encoded = encode_data(parser, &type_, &message_types, &value, Some(&*field.name))?;
|
||||
tokens.append(&mut encoded);
|
||||
}
|
||||
|
||||
keccak(tokens).to_vec()
|
||||
}
|
||||
|
||||
Type::Bytes => {
|
||||
let string = value.as_str().ok_or_else(|| serde_error("string", field_name))?;
|
||||
|
||||
check_hex(&string)?;
|
||||
|
||||
let bytes = (&string[2..])
|
||||
.from_hex::<Vec<u8>>()
|
||||
.map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?;
|
||||
let bytes = keccak(&bytes).to_vec();
|
||||
|
||||
encode(&[EthAbiToken::FixedBytes(bytes)])
|
||||
}
|
||||
|
||||
Type::Byte(_) => {
|
||||
let string = value.as_str().ok_or_else(|| serde_error("string", field_name))?;
|
||||
|
||||
check_hex(&string)?;
|
||||
|
||||
let mut bytes = (&string[2..])
|
||||
.from_hex::<Vec<u8>>()
|
||||
.map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?;
|
||||
|
||||
encode(&[EthAbiToken::FixedBytes(bytes)])
|
||||
}
|
||||
|
||||
Type::String => {
|
||||
let value = value.as_str().ok_or_else(|| serde_error("string", field_name))?;
|
||||
let hash = keccak(value).to_vec();
|
||||
encode(&[EthAbiToken::FixedBytes(hash)])
|
||||
}
|
||||
|
||||
Type::Bool => encode(&[EthAbiToken::Bool(value.as_bool().ok_or_else(|| serde_error("bool", field_name))?)]),
|
||||
|
||||
Type::Address => {
|
||||
let addr = value.as_str().ok_or_else(|| serde_error("string", field_name))?;
|
||||
if addr.len() != 42 {
|
||||
return Err(ErrorKind::InvalidAddressLength(addr.len()))?;
|
||||
}
|
||||
let address = EthAddress::from_str(&addr[2..]).map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?;
|
||||
encode(&[EthAbiToken::Address(address)])
|
||||
}
|
||||
|
||||
Type::Uint | Type::Int => {
|
||||
let string = value.as_str().ok_or_else(|| serde_error("int/uint", field_name))?;
|
||||
|
||||
check_hex(&string)?;
|
||||
|
||||
let uint = U256::from_str(&string[2..]).map_err(|err| ErrorKind::HexParseError(format!("{}", err)))?;
|
||||
|
||||
let token = if *message_type == Type::Uint {
|
||||
EthAbiToken::Uint(uint)
|
||||
} else {
|
||||
EthAbiToken::Int(uint)
|
||||
};
|
||||
encode(&[token])
|
||||
}
|
||||
|
||||
_ => return Err(ErrorKind::UnknownType(format!("{}", field_name.unwrap_or("")), format!("{}", *message_type)))?
|
||||
};
|
||||
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
/// encodes and hashes the given EIP712 struct
|
||||
pub fn hash_structured_data(typed_data: EIP712) -> Result<H256> {
|
||||
// validate input
|
||||
typed_data.validate()?;
|
||||
// EIP-191 compliant
|
||||
let prefix = (b"\x19\x01").to_vec();
|
||||
let domain = to_value(&typed_data.domain).unwrap();
|
||||
let parser = Parser::new();
|
||||
let (domain_hash, data_hash) = (
|
||||
encode_data(&parser, &Type::Custom("EIP712Domain".into()), &typed_data.types, &domain, None)?,
|
||||
encode_data(&parser, &Type::Custom(typed_data.primary_type), &typed_data.types, &typed_data.message, None)?
|
||||
);
|
||||
let concat = [&prefix[..], &domain_hash[..], &data_hash[..]].concat();
|
||||
Ok(keccak(concat))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::from_str;
|
||||
use rustc_hex::ToHex;
|
||||
|
||||
const JSON: &'static str = r#"{
|
||||
"primaryType": "Mail",
|
||||
"domain": {
|
||||
"name": "Ether Mail",
|
||||
"version": "1",
|
||||
"chainId": "0x1",
|
||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
||||
},
|
||||
"message": {
|
||||
"from": {
|
||||
"name": "Cow",
|
||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||
},
|
||||
"to": {
|
||||
"name": "Bob",
|
||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||
},
|
||||
"contents": "Hello, Bob!"
|
||||
},
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "uint256" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Person": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "wallet", "type": "address" }
|
||||
],
|
||||
"Mail": [
|
||||
{ "name": "from", "type": "Person" },
|
||||
{ "name": "to", "type": "Person" },
|
||||
{ "name": "contents", "type": "string" }
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
#[test]
|
||||
fn test_build_dependencies() {
|
||||
let string = r#"{
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "uint256" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Person": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "wallet", "type": "address" }
|
||||
],
|
||||
"Mail": [
|
||||
{ "name": "from", "type": "Person" },
|
||||
{ "name": "to", "type": "Person" },
|
||||
{ "name": "contents", "type": "string" }
|
||||
]
|
||||
}"#;
|
||||
|
||||
let value = from_str::<MessageTypes>(string).expect("alas error!");
|
||||
let mail = "Mail";
|
||||
let person = "Person";
|
||||
|
||||
let hashset = {
|
||||
let mut temp = HashSet::new();
|
||||
temp.insert(mail);
|
||||
temp.insert(person);
|
||||
temp
|
||||
};
|
||||
assert_eq!(build_dependencies(mail, &value), Some(hashset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_type() {
|
||||
let string = r#"{
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "uint256" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Person": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "wallet", "type": "address" }
|
||||
],
|
||||
"Mail": [
|
||||
{ "name": "from", "type": "Person" },
|
||||
{ "name": "to", "type": "Person" },
|
||||
{ "name": "contents", "type": "string" }
|
||||
]
|
||||
}"#;
|
||||
|
||||
let value = from_str::<MessageTypes>(string).expect("alas error!");
|
||||
let mail = &String::from("Mail");
|
||||
assert_eq!(
|
||||
"Mail(Person from,Person to,string contents)Person(string name,address wallet)",
|
||||
encode_type(&mail, &value).expect("alas error!")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_type_hash() {
|
||||
let string = r#"{
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "uint256" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Person": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "wallet", "type": "address" }
|
||||
],
|
||||
"Mail": [
|
||||
{ "name": "from", "type": "Person" },
|
||||
{ "name": "to", "type": "Person" },
|
||||
{ "name": "contents", "type": "string" }
|
||||
]
|
||||
}"#;
|
||||
|
||||
let value = from_str::<MessageTypes>(string).expect("alas error!");
|
||||
let mail = &String::from("Mail");
|
||||
let hash = (type_hash(&mail, &value).expect("alas error!").0).to_hex::<String>();
|
||||
assert_eq!(
|
||||
hash,
|
||||
"a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_data() {
|
||||
let typed_data = from_str::<EIP712>(JSON).expect("alas error!");
|
||||
assert_eq!(
|
||||
hash_structured_data(typed_data).expect("alas error!").to_hex::<String>(),
|
||||
"be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unequal_array_lengths() {
|
||||
const TEST: &'static str = r#"{
|
||||
"primaryType": "Mail",
|
||||
"domain": {
|
||||
"name": "Ether Mail",
|
||||
"version": "1",
|
||||
"chainId": "0x1",
|
||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
||||
},
|
||||
"message": {
|
||||
"from": {
|
||||
"name": "Cow",
|
||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||
},
|
||||
"to": [{
|
||||
"name": "Bob",
|
||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||
}],
|
||||
"contents": "Hello, Bob!"
|
||||
},
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "version", "type": "string" },
|
||||
{ "name": "chainId", "type": "uint256" },
|
||||
{ "name": "verifyingContract", "type": "address" }
|
||||
],
|
||||
"Person": [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "wallet", "type": "address" }
|
||||
],
|
||||
"Mail": [
|
||||
{ "name": "from", "type": "Person" },
|
||||
{ "name": "to", "type": "Person[2]" },
|
||||
{ "name": "contents", "type": "string" }
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let typed_data = from_str::<EIP712>(TEST).expect("alas error!");
|
||||
assert_eq!(
|
||||
hash_structured_data(typed_data).unwrap_err().kind(),
|
||||
ErrorKind::UnequalArrayItems(2, "Person[2]".into(), 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
125
util/EIP-712/src/error.rs
Normal file
125
util/EIP-712/src/error.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2015-2018 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/>.
|
||||
|
||||
use std::fmt::{self, Display};
|
||||
use failure::{Fail, Context, Backtrace};
|
||||
use validator::ValidationErrors;
|
||||
use validator::ValidationErrorsKind;
|
||||
|
||||
pub(crate) type Result<T> = ::std::result::Result<T, Error>;
|
||||
/// Error type
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
inner: Context<ErrorKind>,
|
||||
}
|
||||
/// Possible errors encountered while hashing/encoding an EIP-712 compliant data structure
|
||||
#[derive(Clone, Fail, Debug, PartialEq)]
|
||||
pub enum ErrorKind {
|
||||
/// if we fail to deserialize from a serde::Value as a type specified in message types
|
||||
/// fail with this error.
|
||||
#[fail(display = "Expected type '{}' for field '{}'", _0, _1)]
|
||||
UnexpectedType(String, String),
|
||||
/// the primary type supplied doesn't exist in the MessageTypes
|
||||
#[fail(display = "The given primaryType wasn't found in the types field")]
|
||||
NonExistentType,
|
||||
/// an invalid address was encountered during encoding
|
||||
#[fail(display = "Address string should be a 0x-prefixed 40 character string, got '{}'", _0)]
|
||||
InvalidAddressLength(usize),
|
||||
/// a hex parse error occured
|
||||
#[fail(display = "Failed to parse hex '{}'", _0)]
|
||||
HexParseError(String),
|
||||
/// the field was declared with a unknown type
|
||||
#[fail(display = "The field '{}' has an unknown type '{}'", _0, _1)]
|
||||
UnknownType(String, String),
|
||||
/// Unexpected token
|
||||
#[fail(display = "Unexpected token '{}' while parsing typename '{}'", _0, _1)]
|
||||
UnexpectedToken(String, String),
|
||||
/// the user has attempted to define a typed array with a depth > 10
|
||||
#[fail(display = "Maximum depth for nested arrays is 10")]
|
||||
UnsupportedArrayDepth,
|
||||
/// FieldType validation error
|
||||
#[fail(display = "{}", _0)]
|
||||
ValidationError(String),
|
||||
/// the typed array defined in message types was declared with a fixed length
|
||||
/// that is of unequal length with the items to be encoded
|
||||
#[fail(display = "Expected {} items for array type {}, got {} items", _0, _1, _2)]
|
||||
UnequalArrayItems(u64, String, u64),
|
||||
/// Typed array length doesn't fit into a u64
|
||||
#[fail(display = "Attempted to declare fixed size with length {}", _0)]
|
||||
InvalidArraySize(String)
|
||||
}
|
||||
|
||||
pub(crate) fn serde_error(expected: &str, field: Option<&str>) -> ErrorKind {
|
||||
ErrorKind::UnexpectedType(expected.to_owned(), field.unwrap_or("").to_owned())
|
||||
}
|
||||
|
||||
impl Fail for Error {
|
||||
fn cause(&self) -> Option<&Fail> {
|
||||
self.inner.cause()
|
||||
}
|
||||
|
||||
fn backtrace(&self) -> Option<&Backtrace> {
|
||||
self.inner.backtrace()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Display::fmt(&self.inner, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// extract the error kind
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
self.inner.get_context().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for Error {
|
||||
fn from(kind: ErrorKind) -> Error {
|
||||
Error { inner: Context::new(kind) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Context<ErrorKind>> for Error {
|
||||
fn from(inner: Context<ErrorKind>) -> Error {
|
||||
Error { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValidationErrors> for Error {
|
||||
fn from(error: ValidationErrors) -> Self {
|
||||
let mut string: String = "".into();
|
||||
for (field_name, error_kind) in error.errors() {
|
||||
match error_kind {
|
||||
ValidationErrorsKind::Field(validation_errors) => {
|
||||
for error in validation_errors {
|
||||
let str_error = format!(
|
||||
"the field '{}', has an invalid value {}",
|
||||
field_name,
|
||||
error.params["value"]
|
||||
);
|
||||
string.push_str(&str_error);
|
||||
}
|
||||
},
|
||||
_ => unreachable!("#[validate] is only used on fields for regex;\
|
||||
its impossible to get any other ErrorKind; qed")
|
||||
}
|
||||
}
|
||||
ErrorKind::ValidationError(string).into()
|
||||
}
|
||||
}
|
||||
192
util/EIP-712/src/lib.rs
Normal file
192
util/EIP-712/src/lib.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright 2015-2018 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/>.
|
||||
|
||||
//! EIP-712 encoding utilities
|
||||
//!
|
||||
//! # Specification
|
||||
//!
|
||||
//! `encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)`
|
||||
//! - data adheres to 𝕊, a structure defined in the rigorous eip-712
|
||||
//! - `\x01` is needed to comply with EIP-191
|
||||
//! - `domainSeparator` and `hashStruct` are defined below
|
||||
//!
|
||||
//! ## A) domainSeparator
|
||||
//!
|
||||
//! `domainSeparator = hashStruct(eip712Domain)`
|
||||
//! <br/>
|
||||
//! <br/>
|
||||
//! Struct named `EIP712Domain` with the following fields
|
||||
//!
|
||||
//! - `name: String`
|
||||
//! - `version: String`
|
||||
//! - `chain_id: U256`,
|
||||
//! - `verifying_contract: H160`
|
||||
//! - `salt: Option<H256>`
|
||||
//!
|
||||
//! ## C) hashStruct
|
||||
//!
|
||||
//! `hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))`
|
||||
//! <br/>
|
||||
//! `typeHash = keccak256(encodeType(typeOf(s)))`
|
||||
//!
|
||||
//! ### i) encodeType
|
||||
//!
|
||||
//! - `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"`
|
||||
//! - each member is written as `type ‖ " " ‖ name`
|
||||
//! - encodings cascade down and are sorted by name
|
||||
//!
|
||||
//! ### ii) encodeData
|
||||
//!
|
||||
//! - `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)`
|
||||
//! - each encoded member is 32-byte long
|
||||
//!
|
||||
//! #### a) atomic
|
||||
//!
|
||||
//! - `boolean` => `U256`
|
||||
//! - `address` => `H160`
|
||||
//! - `uint` => sign-extended `U256` in big endian order
|
||||
//! - `bytes1:31` => `H@256`
|
||||
//!
|
||||
//! #### b) dynamic
|
||||
//!
|
||||
//! - `bytes` => `keccak256(bytes)`
|
||||
//! - `string` => `keccak256(string)`
|
||||
//!
|
||||
//! #### c) referenced
|
||||
//!
|
||||
//! - `array` => `keccak256(encodeData(array))`
|
||||
//! - `struct` => `rec(keccak256(hashStruct(struct)))`
|
||||
//!
|
||||
//! ## D) Example
|
||||
//! ### Query
|
||||
//! ```json
|
||||
//! {
|
||||
//! "jsonrpc": "2.0",
|
||||
//! "method": "eth_signTypedData",
|
||||
//! "params": [
|
||||
//! "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
|
||||
//! {
|
||||
//! "types": {
|
||||
//! "EIP712Domain": [
|
||||
//! {
|
||||
//! "name": "name",
|
||||
//! "type": "string"
|
||||
//! },
|
||||
//! {
|
||||
//! "name": "version",
|
||||
//! "type": "string"
|
||||
//! },
|
||||
//! {
|
||||
//! "name": "chainId",
|
||||
//! "type": "uint256"
|
||||
//! },
|
||||
//! {
|
||||
//! "name": "verifyingContract",
|
||||
//! "type": "address"
|
||||
//! }
|
||||
//! ],
|
||||
//! "Person": [
|
||||
//! {
|
||||
//! "name": "name",
|
||||
//! "type": "string"
|
||||
//! },
|
||||
//! {
|
||||
//! "name": "wallet",
|
||||
//! "type": "address"
|
||||
//! }
|
||||
//! ],
|
||||
//! "Mail": [
|
||||
//! {
|
||||
//! "name": "from",
|
||||
//! "type": "Person"
|
||||
//! },
|
||||
//! {
|
||||
//! "name": "to",
|
||||
//! "type": "Person"
|
||||
//! },
|
||||
//! {
|
||||
//! "name": "contents",
|
||||
//! "type": "string"
|
||||
//! }
|
||||
//! ]
|
||||
//! },
|
||||
//! "primaryType": "Mail",
|
||||
//! "domain": {
|
||||
//! "name": "Ether Mail",
|
||||
//! "version": "1",
|
||||
//! "chainId": 1,
|
||||
//! "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
||||
//! },
|
||||
//! "message": {
|
||||
//! "from": {
|
||||
//! "name": "Cow",
|
||||
//! "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||
//! },
|
||||
//! "to": {
|
||||
//! "name": "Bob",
|
||||
//! "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||
//! },
|
||||
//! "contents": "Hello, Bob!"
|
||||
//! }
|
||||
//! }
|
||||
//! ],
|
||||
//! "id": 1
|
||||
//! }
|
||||
//! ```
|
||||
//
|
||||
//! ### Response
|
||||
//! ```json
|
||||
//! {
|
||||
//! "id":1,
|
||||
//! "jsonrpc": "2.0",
|
||||
//! "result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![warn(missing_docs, unused_extern_crates)]
|
||||
|
||||
extern crate serde_json;
|
||||
extern crate ethabi;
|
||||
extern crate ethereum_types;
|
||||
extern crate keccak_hash;
|
||||
extern crate itertools;
|
||||
extern crate failure;
|
||||
extern crate indexmap;
|
||||
extern crate lunarity_lexer;
|
||||
extern crate toolshed;
|
||||
extern crate regex;
|
||||
extern crate validator;
|
||||
#[macro_use]
|
||||
extern crate validator_derive;
|
||||
#[macro_use]
|
||||
extern crate failure_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
extern crate rustc_hex;
|
||||
|
||||
mod eip712;
|
||||
mod error;
|
||||
mod parser;
|
||||
mod encode;
|
||||
|
||||
/// the EIP-712 encoding function
|
||||
pub use encode::hash_structured_data;
|
||||
/// encoding Error types
|
||||
pub use error::{ErrorKind, Error};
|
||||
/// EIP712 struct
|
||||
pub use eip712::EIP712;
|
||||
172
util/EIP-712/src/parser.rs
Normal file
172
util/EIP-712/src/parser.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright 2015-2018 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/>.
|
||||
|
||||
//! Solidity type-name parsing
|
||||
use lunarity_lexer::{Lexer, Token};
|
||||
use error::*;
|
||||
use toolshed::Arena;
|
||||
use std::{fmt, result};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Type {
|
||||
Address,
|
||||
Uint,
|
||||
Int,
|
||||
String,
|
||||
Bool,
|
||||
Bytes,
|
||||
Byte(u8),
|
||||
Custom(String),
|
||||
Array {
|
||||
length: Option<u64>,
|
||||
inner: Box<Type>
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Type> for String {
|
||||
fn from(field_type: Type) -> String {
|
||||
match field_type {
|
||||
Type::Address => "address".into(),
|
||||
Type::Uint => "uint".into(),
|
||||
Type::Int => "int".into(),
|
||||
Type::String => "string".into(),
|
||||
Type::Bool => "bool".into(),
|
||||
Type::Bytes => "bytes".into(),
|
||||
Type::Byte(len) => format!("bytes{}", len),
|
||||
Type::Custom(custom) => custom,
|
||||
Type::Array {
|
||||
inner,
|
||||
length
|
||||
} => {
|
||||
let inner: String = (*inner).into();
|
||||
match length {
|
||||
None => format!("{}[]", inner),
|
||||
Some(length) => format!("{}[{}]", inner, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
|
||||
let item: String = self.clone().into();
|
||||
write!(f, "{}", item)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Parser {
|
||||
arena: Arena,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new() -> Self {
|
||||
Parser {
|
||||
arena: Arena::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// the type string is being validated before it's parsed.
|
||||
pub fn parse_type(&self, field_type: &str) -> Result<Type> {
|
||||
#[derive(PartialEq)]
|
||||
enum State { Open, Close }
|
||||
|
||||
let mut lexer = Lexer::new(&self.arena, field_type);
|
||||
let mut token = None;
|
||||
let mut state = State::Close;
|
||||
let mut array_depth = 0;
|
||||
let mut current_array_length: Option<u64> = None;
|
||||
|
||||
while lexer.token != Token::EndOfProgram {
|
||||
let type_ = match lexer.token {
|
||||
Token::Identifier => Type::Custom(lexer.token_as_str().to_owned()),
|
||||
Token::TypeByte => Type::Byte(lexer.type_size.0),
|
||||
Token::TypeBytes => Type::Bytes,
|
||||
Token::TypeBool => Type::Bool,
|
||||
Token::TypeUint => Type::Uint,
|
||||
Token::TypeInt => Type::Int,
|
||||
Token::TypeString => Type::String,
|
||||
Token::TypeAddress => Type::Address,
|
||||
Token::LiteralInteger => {
|
||||
let length = lexer.token_as_str();
|
||||
current_array_length = Some(length
|
||||
.parse()
|
||||
.map_err(|_|
|
||||
ErrorKind::InvalidArraySize(length.into())
|
||||
)?
|
||||
);
|
||||
lexer.consume();
|
||||
continue;
|
||||
},
|
||||
Token::BracketOpen if token.is_some() && state == State::Close => {
|
||||
state = State::Open;
|
||||
lexer.consume();
|
||||
continue
|
||||
}
|
||||
Token::BracketClose if array_depth < 10 => {
|
||||
if state == State::Open && token.is_some() {
|
||||
let length = current_array_length.take();
|
||||
state = State::Close;
|
||||
token = Some(Type::Array {
|
||||
inner: Box::new(token.expect("if statement checks for some; qed")),
|
||||
length
|
||||
});
|
||||
lexer.consume();
|
||||
array_depth += 1;
|
||||
continue
|
||||
} else {
|
||||
return Err(ErrorKind::UnexpectedToken(lexer.token_as_str().to_owned(), field_type.to_owned()))?
|
||||
}
|
||||
}
|
||||
Token::BracketClose if array_depth == 10 => {
|
||||
return Err(ErrorKind::UnsupportedArrayDepth)?
|
||||
}
|
||||
_ => return Err(ErrorKind::UnexpectedToken(lexer.token_as_str().to_owned(), field_type.to_owned()))?
|
||||
};
|
||||
|
||||
token = Some(type_);
|
||||
lexer.consume();
|
||||
}
|
||||
|
||||
Ok(token.ok_or_else(|| ErrorKind::NonExistentType)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let parser = Parser::new();
|
||||
let source = "byte[][][7][][][][][][][]";
|
||||
parser.parse_type(source).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_array() {
|
||||
let parser = Parser::new();
|
||||
let source = "byte[][][7][][][][][][][][]";
|
||||
assert_eq!(parser.parse_type(source).is_err(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_malformed_array_type() {
|
||||
let parser = Parser::new();
|
||||
let source = "byte[7[]uint][]";
|
||||
assert_eq!(parser.parse_type(source).is_err(), true)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
[package]
|
||||
name = "parity-version"
|
||||
# NOTE: this value is used for Parity Ethereum version string (via env CARGO_PKG_VERSION)
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user