feat: allow encoding/decoding function data (#90)

* feat: allow encoding/decoding function data

* feat: allow decoding event data

* feat: human readable abi

inspired from https://blog.ricmoo.com/human-readable-contract-abis-in-ethers-js-141902f4d917

* test: add event / fn decoding tests

* chore: fix clippy

* feat(abigen): allow providing args in human readable format
This commit is contained in:
Georgios Konstantopoulos 2020-10-29 09:48:24 +02:00 committed by GitHub
parent 35e24ed412
commit eb26915081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 557 additions and 42 deletions

View File

@ -7,7 +7,10 @@ mod types;
use super::util; use super::util;
use super::Abigen; use super::Abigen;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use ethers_core::{abi::Abi, types::Address}; use ethers_core::{
abi::{parse_abi, Abi},
types::Address,
};
use inflector::Inflector; use inflector::Inflector;
use proc_macro2::{Ident, Literal, TokenStream}; use proc_macro2::{Ident, Literal, TokenStream};
use quote::quote; use quote::quote;
@ -22,6 +25,9 @@ pub(crate) struct Context {
/// The parsed ABI. /// The parsed ABI.
abi: Abi, abi: Abi,
/// Was the ABI in human readable format?
human_readable: bool,
/// The contract name as an identifier. /// The contract name as an identifier.
contract_name: Ident, contract_name: Ident,
@ -92,13 +98,23 @@ impl Context {
fn from_abigen(args: Abigen) -> Result<Self> { fn from_abigen(args: Abigen) -> Result<Self> {
// get the actual ABI string // get the actual ABI string
let abi_str = args.abi_source.get().context("failed to get ABI JSON")?; let abi_str = args.abi_source.get().context("failed to get ABI JSON")?;
// parse it // parse it
let abi: Abi = serde_json::from_str(&abi_str) let (abi, human_readable): (Abi, _) = if let Ok(abi) = serde_json::from_str(&abi_str) {
.with_context(|| format!("invalid artifact JSON '{}'", abi_str)) // normal abi format
.with_context(|| { (abi, false)
format!("failed to parse artifact from source {:?}", args.abi_source,) } else {
})?; // heuristic for parsing the human readable format
// replace bad chars
let abi_str = abi_str.replace('[', "").replace(']', "").replace(',', "");
// split lines and get only the non-empty things
let split: Vec<&str> = abi_str
.split('\n')
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.collect();
(parse_abi(&split)?, true)
};
let contract_name = util::ident(&args.contract_name); let contract_name = util::ident(&args.contract_name);
@ -125,6 +141,7 @@ impl Context {
Ok(Context { Ok(Context {
abi, abi,
human_readable,
abi_str: Literal::string(&abi_str), abi_str: Literal::string(&abi_str),
contract_name, contract_name,
method_aliases, method_aliases,

View File

@ -15,7 +15,7 @@ pub(crate) fn imports(name: &str) -> TokenStream {
use std::sync::Arc; use std::sync::Arc;
use ethers::{ use ethers::{
core::{ core::{
abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable}, abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable, parse_abi},
types::*, // import all the types so that we can codegen for everything types::*, // import all the types so that we can codegen for everything
}, },
contract::{Contract, builders::{ContractCall, Event}, Lazy}, contract::{Contract, builders::{ContractCall, Event}, Lazy},
@ -28,10 +28,29 @@ pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) ->
let name = &cx.contract_name; let name = &cx.contract_name;
let abi = &cx.abi_str; let abi = &cx.abi_str;
let abi_parse = if !cx.human_readable {
quote! {
pub static #abi_name: Lazy<Abi> = Lazy::new(|| serde_json::from_str(#abi)
.expect("invalid abi"));
}
} else {
quote! {
pub static #abi_name: Lazy<Abi> = Lazy::new(|| {
let abi_str = #abi.replace('[', "").replace(']', "").replace(',', "");
// split lines and get only the non-empty things
let split: Vec<&str> = abi_str
.split("\n")
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.collect();
parse_abi(&split).expect("invalid abi")
});
}
};
quote! { quote! {
// Inline ABI declaration // Inline ABI declaration
pub static #abi_name: Lazy<Abi> = Lazy::new(|| serde_json::from_str(#abi) #abi_parse
.expect("invalid abi"));
// Struct declaration // Struct declaration
#[derive(Clone)] #[derive(Clone)]

View File

@ -1,12 +1,27 @@
use crate::Contract; use crate::Contract;
use ethers_core::{ use ethers_core::{
abi::{Abi, FunctionExt}, abi::{
types::{Address, Selector}, Abi, Detokenize, Error, Event, Function, FunctionExt, InvalidOutputType, RawLog, Tokenize,
},
types::{Address, Bytes, Selector, H256},
}; };
use ethers_providers::Middleware; use ethers_providers::Middleware;
use rustc_hex::ToHex;
use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc}; use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AbiError {
/// Thrown when the ABI decoding fails
#[error(transparent)]
DecodingError(#[from] ethers_core::abi::Error),
/// Thrown when detokenizing an argument
#[error(transparent)]
DetokenizationError(#[from] InvalidOutputType),
}
/// A reduced form of `Contract` which just takes the `abi` and produces /// A reduced form of `Contract` which just takes the `abi` and produces
/// ABI encoded data for its functions. /// ABI encoded data for its functions.
@ -30,6 +45,68 @@ impl From<Abi> for BaseContract {
} }
impl BaseContract { impl BaseContract {
/// Returns the ABI encoded data for the provided function and arguments
///
/// If the function exists multiple times and you want to use one of the overloaded
/// versions, consider using `encode_with_selector`
pub fn encode<T: Tokenize>(&self, name: &str, args: T) -> Result<Bytes, AbiError> {
let function = self.abi.function(name)?;
encode_fn(function, args)
}
/// Returns the ABI encoded data for the provided function selector and arguments
pub fn encode_with_selector<T: Tokenize>(
&self,
signature: Selector,
args: T,
) -> Result<Bytes, AbiError> {
let function = self.get_from_signature(signature)?;
encode_fn(function, args)
}
/// Decodes the provided ABI encoded function arguments with the selected function name.
///
/// If the function exists multiple times and you want to use one of the overloaded
/// versions, consider using `decode_with_selector`
pub fn decode<D: Detokenize>(
&self,
name: &str,
bytes: impl AsRef<[u8]>,
) -> Result<D, AbiError> {
let function = self.abi.function(name)?;
decode_fn(function, bytes, true)
}
/// Decodes for a given event name, given the `log.topics` and
/// `log.data` fields from the transaction receipt
pub fn decode_event<D: Detokenize>(
&self,
name: &str,
topics: Vec<H256>,
data: Bytes,
) -> Result<D, AbiError> {
let event = self.abi.event(name)?;
decode_event(event, topics, data)
}
/// Decodes the provided ABI encoded bytes with the selected function selector
pub fn decode_with_selector<D: Detokenize>(
&self,
signature: Selector,
bytes: impl AsRef<[u8]>,
) -> Result<D, AbiError> {
let function = self.get_from_signature(signature)?;
decode_fn(function, bytes, true)
}
fn get_from_signature(&self, signature: Selector) -> Result<&Function, AbiError> {
Ok(self
.methods
.get(&signature)
.map(|(name, index)| &self.abi.functions[name][*index])
.ok_or_else(|| Error::InvalidName(signature.to_hex::<String>()))?)
}
/// Returns a reference to the contract's ABI /// Returns a reference to the contract's ABI
pub fn abi(&self) -> &Abi { pub fn abi(&self) -> &Abi {
&self.abi &self.abi
@ -51,6 +128,49 @@ impl AsRef<Abi> for BaseContract {
} }
} }
pub(crate) fn decode_event<D: Detokenize>(
event: &Event,
topics: Vec<H256>,
data: Bytes,
) -> Result<D, AbiError> {
let tokens = event
.parse_log(RawLog {
topics,
data: data.0,
})?
.params
.into_iter()
.map(|param| param.value)
.collect::<Vec<_>>();
Ok(D::from_tokens(tokens)?)
}
// Helper for encoding arguments for a specific function
pub(crate) fn encode_fn<T: Tokenize>(function: &Function, args: T) -> Result<Bytes, AbiError> {
let tokens = args.into_tokens();
Ok(function.encode_input(&tokens).map(Into::into)?)
}
// Helper for decoding bytes from a specific function
pub(crate) fn decode_fn<D: Detokenize>(
function: &Function,
bytes: impl AsRef<[u8]>,
is_input: bool,
) -> Result<D, AbiError> {
let mut bytes = bytes.as_ref();
if bytes.starts_with(&function.selector()) {
bytes = &bytes[4..];
}
let tokens = if is_input {
function.decode_input(bytes.as_ref())?
} else {
function.decode_output(bytes.as_ref())?
};
Ok(D::from_tokens(tokens)?)
}
/// 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>(
@ -72,3 +192,70 @@ where
}) })
.collect() .collect()
} }
#[cfg(test)]
mod tests {
use super::*;
use ethers_core::{abi::parse_abi, types::U256};
use rustc_hex::FromHex;
#[test]
fn can_parse_function_inputs() {
let abi = BaseContract::from(parse_abi(&[
"function approve(address _spender, uint256 value) external view returns (bool, bool)"
]).unwrap());
let spender = "7a250d5630b4cf539739df2c5dacb4c659f2488d"
.parse::<Address>()
.unwrap();
let amount = U256::MAX;
let encoded = abi.encode("approve", (spender, amount)).unwrap();
assert_eq!(encoded.0.to_hex::<String>(), "095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
let (spender2, amount2): (Address, U256) = abi.decode("approve", encoded).unwrap();
assert_eq!(spender, spender2);
assert_eq!(amount, amount2);
}
#[test]
fn can_parse_events() {
let abi = BaseContract::from(
parse_abi(&[
"event Approval(address indexed owner, address indexed spender, uint256 value)",
])
.unwrap(),
);
let topics = vec![
"8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
"000000000000000000000000e4e60fdf9bf188fa57b7a5022230363d5bd56d08",
"0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d",
]
.into_iter()
.map(|hash| hash.parse::<H256>().unwrap())
.collect::<Vec<_>>();
let data = Bytes::from(
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
.from_hex::<Vec<u8>>()
.unwrap(),
);
let (owner, spender, value): (Address, Address, U256) =
abi.decode_event("Approval", topics, data).unwrap();
assert_eq!(value, U256::MAX);
assert_eq!(
owner,
"e4e60fdf9bf188fa57b7a5022230363d5bd56d08"
.parse::<Address>()
.unwrap()
);
assert_eq!(
spender,
"7a250d5630b4cf539739df2c5dacb4c659f2488d"
.parse::<Address>()
.unwrap()
);
}
}

View File

@ -1,5 +1,6 @@
use super::base::{decode_fn, AbiError};
use ethers_core::{ use ethers_core::{
abi::{Detokenize, Error as AbiError, Function, InvalidOutputType}, abi::{Detokenize, Function, InvalidOutputType},
types::{Address, BlockNumber, Bytes, TransactionRequest, TxHash, U256}, types::{Address, BlockNumber, Bytes, TransactionRequest, TxHash, U256},
}; };
use ethers_providers::Middleware; use ethers_providers::Middleware;
@ -13,7 +14,11 @@ use thiserror::Error as ThisError;
pub enum ContractError<M: Middleware> { pub enum ContractError<M: Middleware> {
/// Thrown when the ABI decoding fails /// Thrown when the ABI decoding fails
#[error(transparent)] #[error(transparent)]
DecodingError(#[from] AbiError), DecodingError(#[from] ethers_core::abi::Error),
/// Thrown when the internal BaseContract errors
#[error(transparent)]
AbiError(#[from] AbiError),
/// Thrown when detokenizing an argument /// Thrown when detokenizing an argument
#[error(transparent)] #[error(transparent)]
@ -114,8 +119,8 @@ where
.await .await
.map_err(ContractError::MiddlewareError)?; .map_err(ContractError::MiddlewareError)?;
let tokens = self.function.decode_output(&bytes.0)?; // decode output
let data = D::from_tokens(tokens)?; let data = decode_fn(&self.function, &bytes, false)?;
Ok(data) Ok(data)
} }

View File

@ -1,4 +1,8 @@
use super::{base::BaseContract, call::ContractCall, event::Event}; use super::{
base::{encode_fn, AbiError, BaseContract},
call::ContractCall,
event::Event,
};
use ethers_core::{ use ethers_core::{
abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize}, abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize},
@ -196,7 +200,7 @@ impl<M: Middleware> Contract<M> {
&self, &self,
name: &str, name: &str,
args: T, args: T,
) -> Result<ContractCall<M, D>, Error> { ) -> Result<ContractCall<M, D>, AbiError> {
// get the function // get the function
let function = self.base_contract.abi.function(name)?; let function = self.base_contract.abi.function(name)?;
self.method_func(function, args) self.method_func(function, args)
@ -208,7 +212,7 @@ impl<M: Middleware> Contract<M> {
&self, &self,
signature: Selector, signature: Selector,
args: T, args: T,
) -> Result<ContractCall<M, D>, Error> { ) -> Result<ContractCall<M, D>, AbiError> {
let function = self let function = self
.base_contract .base_contract
.methods .methods
@ -222,16 +226,13 @@ impl<M: Middleware> Contract<M> {
&self, &self,
function: &Function, function: &Function,
args: T, args: T,
) -> Result<ContractCall<M, D>, Error> { ) -> Result<ContractCall<M, D>, AbiError> {
let tokens = args.into_tokens(); let data = encode_fn(function, args)?;
// create the calldata
let data = function.encode_input(&tokens)?;
// create the tx object // create the tx object
let tx = TransactionRequest { let tx = TransactionRequest {
to: Some(NameOrAddress::Address(self.address)), to: Some(NameOrAddress::Address(self.address)),
data: Some(data.into()), data: Some(data),
..Default::default() ..Default::default()
}; };

View File

@ -1,9 +1,9 @@
use crate::ContractError; use crate::{base::decode_event, ContractError};
use ethers_providers::Middleware; use ethers_providers::Middleware;
use ethers_core::{ use ethers_core::{
abi::{Detokenize, Event as AbiEvent, RawLog}, abi::{Detokenize, Event as AbiEvent},
types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64}, types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64},
}; };
@ -122,20 +122,7 @@ where
} }
fn parse_log(&self, log: Log) -> Result<D, ContractError<M>> { fn parse_log(&self, log: Log) -> Result<D, ContractError<M>> {
// ethabi parses the unindexed and indexed logs together to a Ok(decode_event(self.event, log.topics, log.data)?)
// vector of tokens
let tokens = self
.event
.parse_log(RawLog {
topics: log.topics,
data: log.data.0,
})?
.params
.into_iter()
.map(|param| param.value)
.collect::<Vec<_>>();
// convert the tokens to the requested datatype
Ok(D::from_tokens(tokens)?)
} }
} }

View File

@ -0,0 +1,291 @@
use super::{
param_type::Reader, Abi, Event, EventParam, Function, Param, ParamType, StateMutability,
};
use std::collections::HashMap;
use thiserror::Error;
/// Parses a "human readable abi" string vector
///
/// ```
/// use ethers::abi::parse_abi;
///
/// let abi = parse_abi(&[
/// "function x() external view returns (uint256)",
/// ]).unwrap();
/// ```
pub fn parse(input: &[&str]) -> Result<Abi, ParseError> {
let mut abi = Abi {
constructor: None,
functions: HashMap::new(),
events: HashMap::new(),
receive: false,
fallback: false,
};
for line in input {
if line.contains("function") {
let function = parse_function(&line)?;
abi.functions
.entry(function.name.clone())
.or_default()
.push(function);
} else if line.contains("event") {
let event = parse_event(&line)?;
abi.events
.entry(event.name.clone())
.or_default()
.push(event);
} else if line.starts_with("struct") {
panic!("Got tuple");
} else {
panic!("unknown sig")
}
}
Ok(abi)
}
fn parse_event(event: &str) -> Result<Event, ParseError> {
let split: Vec<&str> = event.split("event ").collect();
let split: Vec<&str> = split[1].split('(').collect();
let name = split[0].trim_end();
let rest = split[1];
let args = rest.replace(")", "");
let anonymous = rest.contains("anonymous");
let inputs = if args.contains(',') {
let args: Vec<&str> = args.split(", ").collect();
args.iter()
.map(|arg| parse_event_arg(arg))
.collect::<Result<Vec<EventParam>, _>>()?
} else {
vec![]
};
Ok(Event {
name: name.to_owned(),
anonymous,
inputs,
})
}
// Parses an event's argument as indexed if neded
fn parse_event_arg(param: &str) -> Result<EventParam, ParseError> {
let tokens: Vec<&str> = param.split(' ').collect();
let kind: ParamType = Reader::read(tokens[0])?;
let (name, indexed) = if tokens.len() == 2 {
(tokens[1], false)
} else {
(tokens[2], true)
};
Ok(EventParam {
name: name.to_owned(),
kind,
indexed,
})
}
fn parse_function(fn_string: &str) -> Result<Function, ParseError> {
let fn_string = fn_string.to_owned();
let delim = if fn_string.starts_with("function ") {
"function "
} else {
" "
};
let split: Vec<&str> = fn_string.split(delim).collect();
let split: Vec<&str> = split[1].split('(').collect();
// function name is the first char
let fn_name = split[0];
// internal args
let args: Vec<&str> = split[1].split(')').collect();
let args: Vec<&str> = args[0].split(", ").collect();
let inputs = args
.into_iter()
.filter(|x| !x.is_empty())
.filter(|x| !x.contains("returns"))
.map(|x| parse_param(x))
.collect::<Result<Vec<Param>, _>>()?;
// return value
let outputs: Vec<Param> = if split.len() > 2 {
let ret = split[2].strip_suffix(")").expect("no right paren");
let ret: Vec<&str> = ret.split(", ").collect();
ret.into_iter()
// remove modifiers etc
.filter(|x| !x.is_empty())
.map(|x| parse_param(x))
.collect::<Result<Vec<Param>, _>>()?
} else {
vec![]
};
Ok(Function {
name: fn_name.to_owned(),
inputs,
outputs,
// this doesn't really matter
state_mutability: StateMutability::Nonpayable,
})
}
// address x
fn parse_param(param: &str) -> Result<Param, ParseError> {
let mut param = param
.split(' ')
.filter(|x| !x.contains("memory") || !x.contains("calldata"));
let kind = param.next().ok_or(ParseError::Kind)?;
let kind: ParamType = Reader::read(kind).unwrap();
// strip memory/calldata from the name
// e.g. uint256[] memory x
let mut name = param.next().unwrap_or_default();
if name == "memory" || name == "calldata" {
name = param.next().ok_or(ParseError::Kind)?;
}
Ok(Param {
name: name.to_owned(),
kind,
})
}
#[derive(Error, Debug)]
pub enum ParseError {
#[error("expected data type")]
Kind,
#[error(transparent)]
ParseError(#[from] super::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_approve() {
let fn_str = "function approve(address _spender, uint256 value) external returns(bool)";
let parsed = parse_function(fn_str).unwrap();
assert_eq!(parsed.name, "approve");
assert_eq!(parsed.inputs[0].name, "_spender");
assert_eq!(parsed.inputs[0].kind, ParamType::Address,);
assert_eq!(parsed.inputs[1].name, "value");
assert_eq!(parsed.inputs[1].kind, ParamType::Uint(256),);
assert_eq!(parsed.outputs[0].name, "");
assert_eq!(parsed.outputs[0].kind, ParamType::Bool);
}
#[test]
fn parses_function_arguments_return() {
let fn_str = "function foo(uint32[] memory x) external view returns (address)";
let parsed = parse_function(fn_str).unwrap();
assert_eq!(parsed.name, "foo");
assert_eq!(parsed.inputs[0].name, "x");
assert_eq!(
parsed.inputs[0].kind,
ParamType::Array(Box::new(ParamType::Uint(32)))
);
assert_eq!(parsed.outputs[0].name, "");
assert_eq!(parsed.outputs[0].kind, ParamType::Address);
}
#[test]
fn parses_function_empty() {
let fn_str = "function foo()";
let parsed = parse_function(fn_str).unwrap();
assert_eq!(parsed.name, "foo");
assert!(parsed.inputs.is_empty());
assert!(parsed.outputs.is_empty());
}
#[test]
fn parses_event() {
assert_eq!(
parse_event("event Foo (address indexed x, uint y, bytes32[] z)").unwrap(),
Event {
anonymous: false,
name: "Foo".to_owned(),
inputs: vec![
EventParam {
name: "x".to_owned(),
kind: ParamType::Address,
indexed: true
},
EventParam {
name: "y".to_owned(),
kind: ParamType::Uint(256),
indexed: false,
},
EventParam {
name: "z".to_owned(),
kind: ParamType::Array(Box::new(ParamType::FixedBytes(32))),
indexed: false,
},
]
}
);
}
#[test]
fn parses_anonymous_event() {
assert_eq!(
parse_event("event Foo() anonymous").unwrap(),
Event {
anonymous: true,
name: "Foo".to_owned(),
inputs: vec![],
}
);
}
#[test]
fn parse_event_input() {
assert_eq!(
parse_event_arg("address indexed x").unwrap(),
EventParam {
name: "x".to_owned(),
kind: ParamType::Address,
indexed: true
}
);
assert_eq!(
parse_event_arg("address x").unwrap(),
EventParam {
name: "x".to_owned(),
kind: ParamType::Address,
indexed: false
}
);
}
#[test]
fn can_parse_functions() {
[
"function foo(uint256[] memory x) external view returns (address)",
"function bar(uint256[] memory x) returns (address)",
"function bar(uint256[] memory x, uint32 y) returns (address, uint256)",
"function bar(uint256[] memory x)",
"function bar()",
]
.iter()
.for_each(|x| {
parse_function(x).unwrap();
});
}
#[test]
fn can_read_backslashes() {
parse(&[
"\"function setValue(string)\"",
"\"function getValue() external view (string)\"",
])
.unwrap();
}
}

View File

@ -8,6 +8,9 @@ pub use ethabi::*;
mod tokens; mod tokens;
pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize}; pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize};
mod human_readable;
pub use human_readable::parse as parse_abi;
/// Extension trait for `ethabi::Function`. /// Extension trait for `ethabi::Function`.
pub trait FunctionExt { pub trait FunctionExt {
/// Compute the method signature in the standard ABI format. This does not /// Compute the method signature in the standard ABI format. This does not

View File

@ -6,9 +6,14 @@ use ethers::{
use std::{convert::TryFrom, sync::Arc, time::Duration}; use std::{convert::TryFrom, sync::Arc, time::Duration};
// Generate the type-safe contract bindings by providing the ABI // Generate the type-safe contract bindings by providing the ABI
// definition in human readable format
abigen!( abigen!(
SimpleContract, SimpleContract,
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"}]"#, r#"[
function setValue(string)
function getValue() external view (string)
event ValueChanged(address indexed author, address indexed oldAuthor, string oldValue, string newValue)
]"#,
event_derives(serde::Deserialize, serde::Serialize) event_derives(serde::Deserialize, serde::Serialize)
); );