use alloy_dyn_abi::{DynSolEvent, DynSolType, DynSolValue, Specifier};
use alloy_json_abi::{ContractObject, Function, JsonAbi, StateMutability};
use alloy_primitives::{Bytes, Log, LogData};
use anyhow::{anyhow, bail, Result};
use std::collections::BTreeMap;
type EventMap = BTreeMap<std::string::String, Vec<alloy_json_abi::Event>>;
#[derive(Debug)]
pub struct EventLog {
pub name: String,
pub decoder: DynSolEvent,
}
impl EventLog {
pub fn decode(&self, log: &LogData) -> Option<(String, DynSolValue)> {
if let Ok(r) = self.decoder.decode_log(log, true) {
let v = DynSolValue::Tuple([r.indexed, r.body].concat());
return Some((self.name.clone(), v));
}
None
}
}
pub struct ContractAbi {
pub abi: JsonAbi,
pub bytecode: Option<Bytes>,
pub events_logs: Vec<EventLog>,
}
fn convert_events(ev: &EventMap) -> Vec<EventLog> {
ev.iter()
.flat_map(|(k, v)| {
v.iter()
.map(|e| EventLog {
name: k.clone(),
decoder: e.resolve().unwrap(),
})
.collect::<Vec<EventLog>>()
})
.collect::<Vec<EventLog>>()
}
impl ContractAbi {
pub fn from_full_json(raw: &str) -> Self {
let co =
serde_json::from_str::<ContractObject>(raw).expect("Abi: failed to parse abi to json");
if co.abi.is_none() {
panic!("Abi: ABI not found in file")
}
if co.bytecode.is_none() {
panic!("Abi: Bytecode not found in file")
}
let abi = co.abi.unwrap();
let evts = convert_events(&abi.events);
Self {
abi,
bytecode: co.bytecode,
events_logs: evts,
}
}
pub fn from_abi_bytecode(raw: &str, bytecode: Option<Vec<u8>>) -> Self {
let abi = serde_json::from_str::<JsonAbi>(raw).expect("Abi: failed to parse abi");
let evts = convert_events(&abi.events);
Self {
abi,
bytecode: bytecode.map(Bytes::from),
events_logs: evts,
}
}
pub fn from_human_readable(input: Vec<&str>) -> Self {
let abi = JsonAbi::parse(input).expect("Abi: Invalid solidity function(s) format");
let evts = convert_events(&abi.events);
Self {
abi,
bytecode: None,
events_logs: evts,
}
}
pub fn extract_logs(&self, logs: Vec<Log>) -> Vec<(String, DynSolValue)> {
let mut results: Vec<(String, DynSolValue)> = Vec::new();
for log in logs {
for e in &self.events_logs {
if let Some(p) = e.decode(&log.data) {
results.push(p);
}
}
}
results
}
pub fn has_function(&self, name: &str) -> bool {
self.abi.functions.contains_key(name)
}
pub fn has_fallback(&self) -> bool {
self.abi.fallback.is_some()
}
pub fn has_receive(&self) -> bool {
self.abi.receive.is_some()
}
pub fn bytecode(&self) -> Option<Vec<u8>> {
self.bytecode.as_ref().map(|b| b.to_vec())
}
pub fn encode_constructor(&self, args: &str) -> Result<(Vec<u8>, bool)> {
let bytecode = match self.bytecode() {
Some(b) => b,
_ => bail!("Abi: Missing contract bytecode!"),
};
let constructor = match &self.abi.constructor {
Some(c) => c,
_ => return Ok((bytecode, false)),
};
let types = constructor
.inputs
.iter()
.map(|i| i.resolve().unwrap())
.collect::<Vec<_>>();
let ty = DynSolType::Tuple(types);
let dynavalues = ty.coerce_str(args).map_err(|_| {
anyhow!("Abi: Error coercing the arguments for the constructor. Check the input argument(s)")
})?;
let encoded_args = dynavalues.abi_encode_params();
let is_payable = matches!(constructor.state_mutability, StateMutability::Payable);
Ok(([bytecode, encoded_args].concat(), is_payable))
}
fn extract(funcs: &Function, args: &str) -> Result<DynSolValue> {
let types = funcs
.inputs
.iter()
.map(|i| i.resolve().unwrap())
.collect::<Vec<_>>();
let ty = DynSolType::Tuple(types);
ty.coerce_str(args).map_err(|_| {
anyhow!(
"Abi: Error coercing the arguments for the function call. Check the input argument(s)"
)
})
}
pub fn encode_function(
&self,
name: &str,
args: &str,
) -> anyhow::Result<(Vec<u8>, bool, Option<DynSolType>)> {
let funcs = match self.abi.function(name) {
Some(funcs) => funcs,
_ => bail!("Abi: Function {} not found in the ABI!", name),
};
for f in funcs {
let result = Self::extract(f, args);
let is_payable = matches!(f.state_mutability, StateMutability::Payable);
if result.is_ok() {
let ty = match f.outputs.len() {
0 => None,
1 => f.outputs.first().unwrap().clone().resolve().ok(),
_ => {
let t = f
.outputs
.iter()
.map(|i| i.resolve().unwrap())
.collect::<Vec<_>>();
Some(DynSolType::Tuple(t))
}
};
let selector = f.selector().to_vec();
let encoded_args = result.unwrap().abi_encode_params();
let all = [selector, encoded_args].concat();
return Ok((all, is_payable, ty));
}
}
Err(anyhow::anyhow!(
"Abi: Arguments to the function do not match what is expected"
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{b256, bytes, Address, FixedBytes, LogData, U256};
use alloy_sol_types::{sol, SolCall};
use hex::FromHex;
sol! {
struct HelloInput {
uint256 value;
address owner;
uint160 beta;
}
contract HelloWorld {
address public owner;
function hello(HelloInput params) external returns (bool);
}
}
sol! {
contract MrOverLoads {
function one() public returns (bool);
function one(uint256);
function one(address, (uint64, uint64)) public returns (address);
}
}
sol! {
struct A {
uint256 value;
address owner;
bool isok;
}
struct B {
bytes data;
}
contract KitchenSink {
function check_types(uint256, bool, address, string, bytes32);
function check_both(A, B);
function check_blend(string, uint160, A);
}
}
#[test]
fn check_constructor_encoding() {
let input = vec!["constructor()"];
let mut abi = ContractAbi::from_human_readable(input);
abi.bytecode = Some(b"hello".into());
assert!(abi.encode_constructor("()").is_ok());
assert!(abi.encode_constructor("(1234)").is_err());
}
#[test]
fn encoding_function_decoder_types() {
let tc = ContractAbi::from_human_readable(vec![
"function a()",
"function b() (uint256)",
"function c() (bool, address, uint256)",
]);
let (_, _, r1) = tc.encode_function("a", "()").unwrap();
let (_, _, r2) = tc.encode_function("b", "()").unwrap();
let (_, _, r3) = tc.encode_function("c", "()").unwrap();
assert_eq!(None, r1);
assert_eq!(Some(DynSolType::Uint(256)), r2);
assert_eq!(
Some(DynSolType::Tuple(vec![
DynSolType::Bool,
DynSolType::Address,
DynSolType::Uint(256)
])),
r3
);
}
#[test]
fn encoding_functions() {
let hello_world = vec!["function hello(tuple(uint256, address, uint160)) (bool)"];
let hw = ContractAbi::from_human_readable(hello_world);
assert!(hw.has_function("hello"));
let addy = Address::with_last_byte(24);
let solencoded = HelloWorld::helloCall {
params: HelloInput {
value: U256::from(10),
owner: addy,
beta: U256::from(1),
},
}
.abi_encode();
assert!(hw.encode_function("bob", "()").is_err());
assert!(hw.encode_function("hello", "(1,2").is_err());
let (cencoded, is_payable, dtype) = hw
.encode_function("hello", &format!("(({}, {}, {}))", 10, addy.to_string(), 1))
.unwrap();
assert!(!is_payable);
assert_eq!(solencoded, cencoded);
assert_eq!(dtype, Some(DynSolType::Bool));
}
#[test]
fn encoding_overloaded_functions() {
let overit = vec![
"function one() (bool)",
"function one(uint256)",
"function one(address, (uint64, uint64)) (address)",
];
let abi = ContractAbi::from_human_readable(overit);
let addy = Address::with_last_byte(24);
let sa = MrOverLoads::one_0Call {}.abi_encode();
let (aa, _, _) = abi.encode_function("one", "()").unwrap();
assert_eq!(sa, aa);
let sb = MrOverLoads::one_1Call { _0: U256::from(1) }.abi_encode();
let (ab, _, _) = abi.encode_function("one", "(1)").unwrap();
assert_eq!(sb, ab);
let sc = MrOverLoads::one_2Call {
_0: addy,
_1: (10u64, 11u64),
}
.abi_encode();
let (ac, _, otype) = abi
.encode_function("one", &format!("({},({},{}))", addy.to_string(), 10, 11))
.unwrap();
assert_eq!(sc, ac);
assert_eq!(Some(DynSolType::Address), otype);
}
#[test]
fn encode_kitchen_sink() {
let addy = "0x023e09e337f5a6c82e62fe5ae4b6396d34930751";
let expected_check_types = KitchenSink::check_typesCall {
_0: U256::from(1),
_1: true,
_2: Address::from_hex(addy).unwrap(),
_3: "bob".to_string(),
_4: FixedBytes::from_slice(&[1u8; 32]),
}
.abi_encode();
let abi = ContractAbi::from_human_readable(vec![
"function check_types(uint256, bool, address, string, bytes32)",
"function check_both(tuple(uint256, address, bool), tuple(bytes))",
"function check_blend(string, uint160, tuple(uint256, address, bool))",
]);
let input = "(1, true, 0x023e09e337f5a6c82e62fe5ae4b6396d34930751, 'bob', 0101010101010101010101010101010101010101010101010101010101010101)";
let (actual, _, _) = abi.encode_function("check_types", input).unwrap();
assert_eq!(expected_check_types, actual);
let expected_check_both = KitchenSink::check_bothCall {
_0: A {
value: U256::from(10),
owner: Address::from_hex(addy).unwrap(),
isok: false,
},
_1: B { data: Bytes::new() },
}
.abi_encode();
let input_both = "((10, 0x023e09e337f5a6c82e62fe5ae4b6396d34930751, false),(0x))";
let (actualboth, _, _) = abi.encode_function("check_both", input_both).unwrap();
assert_eq!(expected_check_both, actualboth);
let expected_check_blend = KitchenSink::check_blendCall {
_0: "bob".to_string(),
_1: U256::from(5),
_2: A {
value: U256::from(10),
owner: Address::from_hex(addy).unwrap(),
isok: false,
},
}
.abi_encode();
let input_blend = "(bob, 5,(10, 0x023e09e337f5a6c82e62fe5ae4b6396d34930751, false))";
let (actualblend, _, _) = abi.encode_function("check_blend", input_blend).unwrap();
assert_eq!(expected_check_blend, actualblend)
}
#[test]
fn test_flatten_event_structure() {
let sample = ContractAbi::from_human_readable(vec![
"event Transfer(address indexed from,address indexed to,uint256 amount)",
"event Transfer(address indexed from) anonymous",
"event Mint(address indexed recip,uint256 amount)",
"event Burn(address indexed recip,uint256 amount)",
]);
assert_eq!(4, sample.events_logs.len());
let transfer = LogData::new_unchecked(
vec![
b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
b256!("000000000000000000000000c2e9f25be6257c210d7adf0d4cd6e3e881ba25f8"),
b256!("0000000000000000000000002b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b"),
],
bytes!("0000000000000000000000000000000000000000000000000000000000000005"),
);
let burn = LogData::new_unchecked(
vec![
b256!("cc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5"),
b256!("000000000000000000000000c2e9f25be6257c210d7adf0d4cd6e3e881ba25f8"),
],
bytes!("0000000000000000000000000000000000000000000000000000000000000005"),
);
let log_address = Address::repeat_byte(14);
let logs = vec![
Log {
address: log_address,
data: transfer,
},
Log {
address: log_address,
data: burn,
},
];
let results = sample.extract_logs(logs);
assert_eq!(2, results.len());
}
}