Skip to content

Commit

Permalink
Parse logged types (FuelLabs#582)
Browse files Browse the repository at this point in the history
Closes FuelLabs#562 

Adds 2 methods for parsing and fetching receipt logs. logs_with_type::<T>() returns all logs of type T. fetch_logs() returns a vector containing all logs as Strings.

Co-authored-by: Halil Beglerović <[email protected]>
Co-authored-by: Mohammad Fawaz <[email protected]>
Co-authored-by: Ahmed Sagdati <[email protected]>
  • Loading branch information
4 people authored Oct 3, 2022
1 parent 450e9ae commit 204b138
Show file tree
Hide file tree
Showing 16 changed files with 485 additions and 84 deletions.
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- [Transaction parameters](./calling-contracts/tx-params.md)
- [Call parameters](./calling-contracts/call-params.md)
- [Call response](./calling-contracts/call-response.md)
- [Logs](./calling-contracts/logs.md)
- [Variable outputs](./calling-contracts/variable-outputs.md)
- [Read-only calls](./calling-contracts/read-only.md)
- [Calling other contracts](./calling-contracts/other-contracts.md)
Expand Down
1 change: 0 additions & 1 deletion docs/src/calling-contracts/call-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Where `value` will hold the value returned by its respective contract method, re

- `receipts` will hold all [receipts](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/abi.md#receipt) generated by that specific contract call.
- `gas_used` is the amount of gas it consumed by the contract call.
- `logs` will hold all logs that happened within that specific contract call.

To log out `receipts` values during testing, you have to run `test` as follows:

Expand Down
25 changes: 25 additions & 0 deletions docs/src/calling-contracts/logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs

Whenever you log a value within a contract method, the resulting log entry is added to the log receipt and the variable type is recorded in the contract's ABI. The SDK lets you parse those values into Rust types.

Consider the following contract method:

```rust,ignore
{{#include ../../../packages/fuels/tests/test_projects/logged_types/src/main.sw:produce_logs}}
```

You can access the logged values in Rust by calling `logs_with_type::<T>` from a contract instance, where `T` is the type of the logged variables you want to retrieve. The result will be a `Vec<T>`:

```rust,ignore
{{#include ../../../packages/fuels/tests/harness.rs:produce_logs}}
```

You can also get a vector of all the logged values as strings using `fetch_logs()`:

```rust, ignore
{{#include ../../../packages/fuels/tests/harness.rs:fetch_logs}}
```

Due to possible performance hits, it is not recommended to use `fetch_logs()` outside of a debugging scenario.

> **Note:** To bind logged values in the SDK, you need to build your contract by supplying a feature flag: `forc build --generate-logged-types`. This is temporary and the flag won't be needed in the future
1 change: 0 additions & 1 deletion examples/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,6 @@ mod tests {
match response {
// The transaction is valid and executes to completion
Ok(call_response) => {
let logs: Vec<String> = call_response.logs;
let receipts: Vec<Receipt> = call_response.receipts;
// Do things with logs and receipts
}
Expand Down
11 changes: 0 additions & 11 deletions packages/fuels-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,10 @@ pub struct CallResponse<D> {
pub value: D,
pub receipts: Vec<Receipt>,
pub gas_used: u64,
pub logs: Vec<String>,
}
// ANCHOR_END: call_response

impl<D> CallResponse<D> {
/// Get all the logs from LogData receipts
fn get_logs(receipts: &[Receipt]) -> Vec<String> {
receipts
.iter()
.filter(|r| matches!(r, Receipt::LogData { .. }))
.map(|r| hex::encode(r.data().unwrap()))
.collect::<Vec<String>>()
}

/// Get the gas used from ScriptResult receipt
fn get_gas_used(receipts: &[Receipt]) -> u64 {
receipts
Expand All @@ -83,7 +73,6 @@ impl<D> CallResponse<D> {
pub fn new(value: D, receipts: Vec<Receipt>) -> Self {
Self {
value,
logs: Self::get_logs(&receipts),
gas_used: Self::get_gas_used(&receipts),
receipts,
}
Expand Down
2 changes: 2 additions & 0 deletions packages/fuels-core/src/code_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ pub mod docs_gen;
pub mod function_selector;
pub mod functions_gen;
mod resolved_type;

pub use abigen::{extract_and_parse_logs, extract_log_ids_and_data};
180 changes: 169 additions & 11 deletions packages/fuels-core/src/code_gen/abigen.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};

use crate::code_gen::bindings::ContractBindings;
use crate::code_gen::custom_types::extract_custom_type_name_from_abi_type_field;
use crate::source::Source;
use crate::utils::ident;
use crate::{try_from_bytes, Parameterize, Tokenizable};
use fuel_tx::Receipt;
use fuels_types::errors::Error;
use fuels_types::{ProgramABI, TypeDeclaration};
use fuels_types::param_types::ParamType;
use fuels_types::{ProgramABI, ResolvedLog, TypeDeclaration};
use itertools::Itertools;
use proc_macro2::{Ident, TokenStream};
use quote::quote;

use super::custom_types::{expand_custom_enum, expand_custom_struct};
use super::custom_types::{expand_custom_enum, expand_custom_struct, single_param_type_call};
use super::functions_gen::expand_function;
use super::resolved_type::resolve_type;

pub struct Abigen {
/// Format the code using a locally installed copy of `rustfmt`.
Expand Down Expand Up @@ -78,6 +83,10 @@ impl Abigen {
let abi_structs = self.abi_structs()?;
let abi_enums = self.abi_enums()?;

let resolved_logs = self.resolve_logs();
let log_id_param_type_pairs = generate_log_id_param_type_pairs(&resolved_logs);
let fetch_logs = generate_fetch_logs(&resolved_logs);

let (includes, code) = if self.no_std {
(
quote! {
Expand All @@ -94,27 +103,32 @@ impl Abigen {
(
quote! {
use fuels::contract::contract::{Contract, ContractCallHandler};
use fuels::core::{EnumSelector, StringToken, Parameterize, Tokenizable, Token,
use fuels::core::{EnumSelector, StringToken, Parameterize, Tokenizable, Token,
Identity, try_from_bytes};
use fuels::core::code_gen::{extract_and_parse_logs, extract_log_ids_and_data};
use fuels::core::abi_decoder::ABIDecoder;
use fuels::core::code_gen::function_selector::resolve_fn_selector;
use fuels::core::types::*;
use fuels::signers::WalletUnlocked;
use fuels::tx::{ContractId, Address};
use fuels::tx::{ContractId, Address, Receipt};
use fuels::types::bech32::Bech32ContractId;
use fuels::types::ResolvedLog;
use fuels::types::errors::Error as SDKError;
use fuels::types::param_types::{EnumVariants, ParamType};
use std::str::FromStr;
use std::collections::{HashSet, HashMap};
},
quote! {
pub struct #name {
contract_id: Bech32ContractId,
wallet: WalletUnlocked
wallet: WalletUnlocked,
logs_lookup: Vec<(u64, ParamType)>,
}

impl #name {
pub fn new(contract_id: String, wallet: WalletUnlocked) -> Self {
let contract_id = Bech32ContractId::from_str(&contract_id).expect("Invalid contract id");
Self { contract_id, wallet }
Self { contract_id, wallet, logs_lookup: vec![#(#log_id_param_type_pairs),*]}
}

pub fn get_contract_id(&self) -> &Bech32ContractId {
Expand All @@ -129,13 +143,21 @@ impl Abigen {
let provider = self.wallet.get_provider()?;
wallet.set_provider(provider.clone());

Ok(Self { contract_id: self.contract_id.clone(), wallet: wallet })
Ok(Self { contract_id: self.contract_id.clone(), wallet: wallet, logs_lookup: self.logs_lookup.clone() })
}

pub fn methods(&self) -> #methods_name {
#methods_name { contract_id: self.contract_id.clone(), wallet: self.wallet.clone() }
pub fn logs_with_type<D: Tokenizable + Parameterize>(&self, receipts: &[Receipt]) -> Result<Vec<D>, SDKError> {
extract_and_parse_logs(&self.logs_lookup, receipts)
}

#fetch_logs

pub fn methods(&self) -> #methods_name {
#methods_name {
contract_id: self.contract_id.clone(),
wallet: self.wallet.clone(),
}
}
}

pub struct #methods_name {
Expand All @@ -145,7 +167,6 @@ impl Abigen {

impl #methods_name {
#contract_functions

}
},
)
Expand Down Expand Up @@ -257,6 +278,143 @@ impl Abigen {
pub fn get_types(abi: &ProgramABI) -> HashMap<usize, TypeDeclaration> {
abi.types.iter().map(|t| (t.type_id, t.clone())).collect()
}

/// Reads the parsed logged types from the ABI and creates ResolvedLogs
fn resolve_logs(&self) -> Vec<ResolvedLog> {
self.abi
.logged_types
.as_ref()
.into_iter()
.flatten()
.map(|l| {
let resolved_type =
resolve_type(&l.application, &self.types).expect("Failed to resolve log type");
let param_type_call = single_param_type_call(&resolved_type);
let resolved_type_name = TokenStream::from(resolved_type);

ResolvedLog {
log_id: l.log_id,
param_type_call,
resolved_type_name,
}
})
.collect()
}
}

pub fn generate_fetch_logs(resolved_logs: &[ResolvedLog]) -> TokenStream {
let generate_method = |body: TokenStream| {
quote! {
pub fn fetch_logs(&self, receipts: &[Receipt]) -> Vec<String> {
#body
}
}
};

// if logs are not present, fetch_logs should return an empty string vec
if resolved_logs.is_empty() {
return generate_method(quote! { vec![] });
}

let branches = generate_param_type_if_branches(resolved_logs);
let body = quote! {
let id_to_param_type: HashMap<_, _> = self.logs_lookup
.iter()
.map(|(id, param_type)| (id, param_type))
.collect();
let ids_with_data = extract_log_ids_and_data(receipts);

ids_with_data
.iter()
.map(|(id, data)|{
let param_type = id_to_param_type.get(id).expect("Failed to find log id.");

#(#branches)else*
else {
panic!("Failed to parse param type.");
}
})
.collect()
};

generate_method(quote! { #body })
}

fn generate_param_type_if_branches(resolved_logs: &[ResolvedLog]) -> Vec<TokenStream> {
resolved_logs
.iter()
.unique_by(|r| r.param_type_call.to_string())
.map(|r| {
let type_name = &r.resolved_type_name;
let param_type_call = &r.param_type_call;

quote! {
if **param_type == #param_type_call {
return format!(
"{:#?}",
try_from_bytes::<#type_name>(&data).expect("Failed to construct type from log data.")
);
}
}
})
.collect()
}

fn generate_log_id_param_type_pairs(resolved_logs: &[ResolvedLog]) -> Vec<TokenStream> {
resolved_logs
.iter()
.map(|r| {
let id = r.log_id;
let param_type_call = &r.param_type_call;

quote! {
(#id, #param_type_call)
}
})
.collect()
}

pub fn extract_and_parse_logs<T: Tokenizable + Parameterize>(
logs_lookup: &[(u64, ParamType)],
receipts: &[Receipt],
) -> Result<Vec<T>, Error> {
let target_param_type = T::param_type();

let target_ids: HashSet<u64> = logs_lookup
.iter()
.filter_map(|(log_id, param_type)| {
if *param_type == target_param_type {
Some(*log_id)
} else {
None
}
})
.collect();

let decoded_logs: Vec<T> = receipts
.iter()
.filter_map(|r| match r {
Receipt::LogData { rb, data, .. } if target_ids.contains(rb) => Some(data.clone()),
Receipt::Log { ra, rb, .. } if target_ids.contains(rb) => {
Some(ra.to_be_bytes().to_vec())
}
_ => None,
})
.map(|data| try_from_bytes(&data))
.collect::<Result<Vec<_>, _>>()?;

Ok(decoded_logs)
}

pub fn extract_log_ids_and_data(receipts: &[Receipt]) -> Vec<(u64, Vec<u8>)> {
receipts
.iter()
.filter_map(|r| match r {
Receipt::LogData { rb, data, .. } => Some((*rb, data.clone())),
Receipt::Log { ra, rb, .. } => Some((*rb, ra.to_be_bytes().to_vec())),
_ => None,
})
.collect()
}

// @todo all (or most, the applicable ones at least) tests in `abigen.rs` should be
Expand Down
5 changes: 4 additions & 1 deletion packages/fuels-core/src/code_gen/custom_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ mod utils;
pub use enum_gen::expand_custom_enum;
pub use struct_gen::expand_custom_struct;
pub(crate) use utils::extract_generic_name;
pub use utils::{extract_custom_type_name_from_abi_type_field, param_type_calls, Component};
pub use utils::{
extract_custom_type_name_from_abi_type_field, param_type_calls, single_param_type_call,
Component,
};

// Doing string -> TokenStream -> string isn't pretty but gives us the opportunity to
// have a better understanding of the generated code so we consider it ok.
Expand Down
33 changes: 19 additions & 14 deletions packages/fuels-core/src/code_gen/custom_types/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub(crate) fn extract_generic_name(field: &str) -> Option<String> {

/// Returns a vector of TokenStreams, one for each of the generic parameters
/// used by the given type.
pub(crate) fn extract_generic_parameters(
pub fn extract_generic_parameters(
type_decl: &TypeDeclaration,
types: &HashMap<usize, TypeDeclaration>,
) -> Result<Vec<TokenStream>, Error> {
Expand Down Expand Up @@ -148,22 +148,27 @@ pub fn extract_custom_type_name_from_abi_type_field(type_field: &str) -> Result<
pub fn param_type_calls(field_entries: &[Component]) -> Vec<TokenStream> {
field_entries
.iter()
.map(|Component { field_type, .. }| {
let type_name = &field_type.type_name;
let parameters = field_type
.generic_params
.iter()
.map(TokenStream::from)
.collect::<Vec<_>>();
if parameters.is_empty() {
quote! { <#type_name>::param_type() }
} else {
quote! { #type_name::<#(#parameters),*>::param_type() }
}
})
.map(|Component { field_type, .. }| single_param_type_call(field_type))
.collect()
}

/// Returns a TokenStream representing the call to `Parameterize::param_type` for
/// the given ResolvedType. Makes sure to properly handle calls when generics are
/// involved.
pub fn single_param_type_call(field_type: &ResolvedType) -> TokenStream {
let type_name = &field_type.type_name;
let parameters = field_type
.generic_params
.iter()
.map(TokenStream::from)
.collect::<Vec<_>>();
if parameters.is_empty() {
quote! { <#type_name>::param_type() }
} else {
quote! { #type_name::<#(#parameters),*>::param_type() }
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading

0 comments on commit 204b138

Please sign in to comment.