Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode Output function implementation #55

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@
# generated parquet data folder
**/data

ex/
ex/

.idea
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ members = [
"examples/watch",
"examples/reverse_wallet",
"examples/call_watch",
"examples/call_decode_output",
]
12 changes: 12 additions & 0 deletions examples/call_decode_output/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "call_decode_output"
version = "0.1.0"
edition = "2021"

[dependencies]
hypersync-client = { path = "../../hypersync-client" }
alloy-json-abi = "0.8.0"
tokio = { version = "1", features = ["full"] }
serde_json = "1"
env_logger = "0.4"
anyhow = "1.0.89"
80 changes: 80 additions & 0 deletions examples/call_decode_output/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use alloy_json_abi::Function;
use hypersync_client::{net_types::Query, simple_types::Trace, ArrowResponseData, CallDecoder, Client, ClientConfig, FromArrow, StreamConfig};
use std::sync::Arc;
use anyhow::Context;

const BALANCE_OF_SIGNATURE : &str= "function balanceOf(address account) external view returns (uint256)";
const DAI_ADDRESS: &str = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init()?;

let client = Arc::new(
Client::new(ClientConfig {
max_num_retries: Some(10),
..ClientConfig::default()
})
.unwrap(),
);

let signature = Function::parse(BALANCE_OF_SIGNATURE.as_ref())
.context("parse function signature")?
.selector();

let query: Query = serde_json::from_value(serde_json::json!({
"from_block": 16291127, // Aave V3 deployment block
"traces": [{
"sighash": [format!("{:}", signature)],
"to": [DAI_ADDRESS],
}],
"field_selection": {
"trace": ["input", "output"],
}
}))
.unwrap();

let decoder = CallDecoder::from_signatures(&[BALANCE_OF_SIGNATURE]).unwrap();

let config = StreamConfig {
..Default::default()
};

let mut rx = client.clone().stream_arrow(query, config).await?;

fn convert_traces(arrow_response_data: ArrowResponseData) -> Vec<Trace> {
arrow_response_data
.traces
.iter()
.flat_map(Trace::from_arrow)
.collect()
}

while let Some(result) = rx.recv().await {
match result {
Ok(response) => {
println!("Received response");
let traces = convert_traces(response.data);
for trace in traces {
if let (Some(input), Some(output)) = (trace.input, trace.output) {
if let Some(args) = decoder.decode_input(&input).context("Failed to decode input") ?{
let address = args[0].as_address().unwrap();
if let Some(results) = decoder.decode_output(&output, BALANCE_OF_SIGNATURE).context("Failed to decode output")? {
if results.len() > 0 {
let (balance,_) = results[0].as_uint().unwrap();
println!("ADDRESS {} : {} DAI", address, balance);

}
}
}

}
}
},
Err(e) => {
eprintln!("Error: {:?}", e);
}
}
}

Ok(())
}
95 changes: 93 additions & 2 deletions hypersync-client/src/decode_call.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
use alloy_dyn_abi::{DynSolType, DynSolValue, JsonAbiExt};
use alloy_json_abi::Function;
use anyhow::{Context, Result};
use hypersync_format::Data;
Expand Down Expand Up @@ -55,8 +55,49 @@ impl CallDecoder {
.context("decoding input data")?;
Ok(Some(decoded))
}

/// Parse output data and return result
///
/// Decodes the output field from a trace
/// and returns the decoded values in a `Vec<DynSolValue>`. If the function
/// signature is not found or the decoding fails, it returns `Ok(None)` as
/// the result to match the behavior of `decode_input`
pub fn decode_output(&self, data: &Data, function_signature: &str) -> Result<Option<Vec<DynSolValue>>>{
// Parse the provided function signature into a Function object
let function = Function::parse(function_signature).context("parsing function signature")?;

//Extract the output types of the function
let output_types = function.outputs;

// Convert the output types into the corresponding DynSolType representations,
let output_types: Vec<DynSolType> = output_types
.into_iter()
.map(|param| param.ty.parse::<DynSolType>())
.collect::<Result<_, _>>() // Parse each type as DynSolType
.context("parsing output types")?;

// Create a tuple type from the output parameters
let tuple_type = DynSolType::Tuple(output_types);

// Attempt to decode the data using the constructed tuple type
match tuple_type.abi_decode(data.as_ref()) {
// If decoding succeeds, return the decoded values as a tuple or single value
Ok(decoded) => {
if let DynSolValue::Tuple(values) = decoded {
Ok(Some(values)) // Return the decoded values as a Vec if it's a tuple
} else {
Ok(Some(vec![decoded])) // Return a single value wrapped in a Vec
}
}
// If decoding fails, return None (to match the behavior of decode_input)
Err(_) => {
Ok(None) // Return None to signal that decoding failed
}
}
}
}


#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -91,7 +132,7 @@ mod tests {
"transfer(address dst, uint256 wad)",
"approve(address usr, uint256 wad)",
])
.unwrap();
.unwrap();
let got = decoder.decode_input(&input).unwrap().unwrap();

for (expected, got) in expected.iter().zip(got.iter()) {
Expand All @@ -104,4 +145,54 @@ mod tests {
fn test_decode_input_with_incorrect_signature() {
let _function = alloy_json_abi::Function::parse("incorrect signature").unwrap();
}

#[test]
fn test_decode_output_single_value() {
let output = "0x0000000000000000000000000000000000000000000000056bc75e2d63100000";
let output = Data::decode_hex(output).unwrap();
let function_signature = "balanceOf(address)(uint256)";

let decoder = CallDecoder::from_signatures(&["balanceOf(address)"]).unwrap();
let result = decoder.decode_output(&output, function_signature).unwrap().unwrap();

assert_eq!(result.len(), 1, "Should return a single value");
assert!(matches!(result[0], DynSolValue::Uint(..)), "Should be a uint value");
}

#[test]
fn test_decode_output_multiple_values() {
let output = "0x000000000000000000000000dc4bde73fa35b7478a574f78d5dfd57a0b2e22810000000000000000000000000000000000000000000000004710ca26d3eeae0a";
let output = Data::decode_hex(output).unwrap();
let function_signature = "someFunction()(address,uint256)";

let decoder = CallDecoder::from_signatures(&["someFunction()"]).unwrap();
let result = decoder.decode_output(&output, function_signature).unwrap().unwrap();

assert_eq!(result.len(), 2, "Should return two values");
assert!(matches!(result[0], DynSolValue::Address(..)), "First value should be an address");
assert!(matches!(result[1], DynSolValue::Uint(..)), "Second value should be a uint");
}

#[test]
fn test_decode_output_invalid_data() {
let output = "invalid_data";
let output = Data::decode_hex(output).unwrap_err();
let function_signature = "balanceOf(address)(uint256)";

let decoder = CallDecoder::from_signatures(&["balanceOf(address)"]).unwrap();
let result = decoder.decode_output(&Data::default(), function_signature).unwrap();

assert!(result.is_none(), "Should return None for invalid data");
}

#[test]
#[should_panic(expected = "parsing function signature")]
fn test_decode_output_invalid_signature() {
let output = "0x0000000000000000000000000000000000000000000000056bc75e2d63100000";
let output = Data::decode_hex(output).unwrap();
let function_signature = "invalid signature";

let decoder = CallDecoder::from_signatures(&["balanceOf(address)"]).unwrap();
decoder.decode_output(&output, function_signature).unwrap();
}
}