feat(substreams): add substreams for Uniswap v2 and v3
This commit is contained in:
103
substreams/ethereum-uniswap-v3/src/modules/1_map_pool_created.rs
Normal file
103
substreams/ethereum-uniswap-v3/src/modules/1_map_pool_created.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use ethabi::ethereum_types::Address;
|
||||
use substreams::scalar::BigInt;
|
||||
use substreams_ethereum::pb::eth::v2::{self as eth};
|
||||
|
||||
use substreams_helper::{event_handler::EventHandler, hex::Hexable};
|
||||
|
||||
use crate::{
|
||||
abi::factory::events::PoolCreated,
|
||||
pb::tycho::evm::v1::{
|
||||
Attribute, BlockChanges, ChangeType, EntityChanges, FinancialType, ImplementationType,
|
||||
ProtocolComponent, ProtocolType, Transaction, TransactionChanges,
|
||||
},
|
||||
};
|
||||
|
||||
#[substreams::handlers::map]
|
||||
pub fn map_pools_created(
|
||||
params: String,
|
||||
block: eth::Block,
|
||||
) -> Result<BlockChanges, substreams::errors::Error> {
|
||||
let mut new_pools: Vec<TransactionChanges> = vec![];
|
||||
let factory_address = params.as_str();
|
||||
|
||||
get_new_pools(&block, &mut new_pools, factory_address);
|
||||
|
||||
Ok(BlockChanges { block: Some(block.into()), changes: new_pools })
|
||||
}
|
||||
|
||||
// Extract new pools from PoolCreated events
|
||||
fn get_new_pools(
|
||||
block: ð::Block,
|
||||
new_pools: &mut Vec<TransactionChanges>,
|
||||
factory_address: &str,
|
||||
) {
|
||||
// Extract new pools from PoolCreated events
|
||||
let mut on_pool_created = |event: PoolCreated, _tx: ð::TransactionTrace, _log: ð::Log| {
|
||||
let tycho_tx: Transaction = _tx.into();
|
||||
|
||||
new_pools.push(TransactionChanges {
|
||||
tx: Some(tycho_tx.clone()),
|
||||
contract_changes: vec![],
|
||||
entity_changes: vec![EntityChanges {
|
||||
component_id: event.pool.to_hex(),
|
||||
attributes: vec![
|
||||
Attribute {
|
||||
name: "liquidity".to_string(),
|
||||
value: BigInt::from(0).to_signed_bytes_le(),
|
||||
change: ChangeType::Creation.into(),
|
||||
},
|
||||
Attribute {
|
||||
name: "tick".to_string(),
|
||||
value: BigInt::from(0).to_signed_bytes_le(),
|
||||
change: ChangeType::Creation.into(),
|
||||
},
|
||||
Attribute {
|
||||
name: "sqrt_price_x96".to_string(),
|
||||
value: BigInt::from(0).to_signed_bytes_le(),
|
||||
change: ChangeType::Creation.into(),
|
||||
},
|
||||
],
|
||||
}],
|
||||
component_changes: vec![ProtocolComponent {
|
||||
id: event.pool.to_hex(),
|
||||
tokens: vec![event.token0, event.token1],
|
||||
contracts: vec![],
|
||||
static_att: vec![
|
||||
Attribute {
|
||||
name: "fee".to_string(),
|
||||
value: event.fee.to_signed_bytes_le(),
|
||||
change: ChangeType::Creation.into(),
|
||||
},
|
||||
Attribute {
|
||||
name: "tick_spacing".to_string(),
|
||||
value: event.tick_spacing.to_signed_bytes_le(),
|
||||
change: ChangeType::Creation.into(),
|
||||
},
|
||||
Attribute {
|
||||
name: "pool_address".to_string(),
|
||||
value: event.pool,
|
||||
change: ChangeType::Creation.into(),
|
||||
},
|
||||
],
|
||||
change: i32::from(ChangeType::Creation),
|
||||
protocol_type: Option::from(ProtocolType {
|
||||
name: "uniswap_v3_pool".to_string(),
|
||||
financial_type: FinancialType::Swap.into(),
|
||||
attribute_schema: vec![],
|
||||
implementation_type: ImplementationType::Custom.into(),
|
||||
}),
|
||||
tx: Some(tycho_tx),
|
||||
}],
|
||||
balance_changes: vec![],
|
||||
})
|
||||
};
|
||||
|
||||
let mut eh = EventHandler::new(block);
|
||||
|
||||
eh.filter_by_address(vec![Address::from_str(factory_address).unwrap()]);
|
||||
|
||||
eh.on::<PoolCreated, _>(&mut on_pool_created);
|
||||
eh.handle_events();
|
||||
}
|
||||
23
substreams/ethereum-uniswap-v3/src/modules/2_store_pools.rs
Normal file
23
substreams/ethereum-uniswap-v3/src/modules/2_store_pools.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::str;
|
||||
|
||||
use substreams::store::{StoreNew, StoreSetIfNotExists, StoreSetIfNotExistsProto};
|
||||
|
||||
use crate::pb::{tycho::evm::v1::BlockChanges, uniswap::v3::Pool};
|
||||
|
||||
#[substreams::handlers::store]
|
||||
pub fn store_pools(pools_created: BlockChanges, store: StoreSetIfNotExistsProto<Pool>) {
|
||||
// Store pools. Required so the next maps can match any event to a known pool by their address
|
||||
|
||||
for change in pools_created.changes {
|
||||
for component_change in &change.component_changes {
|
||||
let pool_address: &str = &component_change.id;
|
||||
let pool: Pool = Pool {
|
||||
address: hex::decode(pool_address.trim_start_matches("0x")).unwrap(),
|
||||
token0: component_change.tokens[0].clone(),
|
||||
token1: component_change.tokens[1].clone(),
|
||||
created_tx_hash: change.tx.as_ref().unwrap().hash.clone(),
|
||||
};
|
||||
store.set_if_not_exists(0, format!("{}:{}", "Pool", pool_address), &pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::Ok;
|
||||
use substreams::store::{StoreGet, StoreGetProto};
|
||||
use substreams_ethereum::pb::eth::v2::{self as eth};
|
||||
use substreams_helper::hex::Hexable;
|
||||
|
||||
use crate::{
|
||||
events::get_log_changed_balances,
|
||||
pb::uniswap::v3::{BalanceDeltas, Pool},
|
||||
};
|
||||
|
||||
#[substreams::handlers::map]
|
||||
pub fn map_balance_changes(
|
||||
block: eth::Block,
|
||||
pools_store: StoreGetProto<Pool>,
|
||||
) -> Result<BalanceDeltas, anyhow::Error> {
|
||||
let mut balances_deltas = Vec::new();
|
||||
for trx in block.transactions() {
|
||||
let mut tx_deltas = Vec::new();
|
||||
for log in trx
|
||||
.calls
|
||||
.iter()
|
||||
.filter(|call| !call.state_reverted)
|
||||
.flat_map(|call| &call.logs)
|
||||
{
|
||||
// Skip if the log is not from a known uniswapV3 pool.
|
||||
if let Some(pool) =
|
||||
pools_store.get_last(format!("{}:{}", "Pool", &log.address.to_hex()))
|
||||
{
|
||||
tx_deltas.extend(get_log_changed_balances(log, &pool))
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !tx_deltas.is_empty() {
|
||||
balances_deltas.extend(tx_deltas);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BalanceDeltas { deltas: balances_deltas })
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::pb::uniswap::v3::BalanceDeltas;
|
||||
use num_bigint::Sign;
|
||||
use substreams::{
|
||||
scalar::BigInt,
|
||||
store::{StoreAdd, StoreAddBigInt, StoreNew},
|
||||
};
|
||||
|
||||
#[substreams::handlers::store]
|
||||
pub fn store_pools_balances(balances_deltas: BalanceDeltas, store: StoreAddBigInt) {
|
||||
let mut deltas = balances_deltas.deltas.clone();
|
||||
|
||||
deltas.sort_unstable_by_key(|delta| delta.ordinal);
|
||||
|
||||
deltas.iter().for_each(|delta| {
|
||||
store.add(
|
||||
delta.ordinal,
|
||||
format!(
|
||||
"pool:{0}:token:{1}",
|
||||
hex::encode(&delta.pool_address),
|
||||
hex::encode(&delta.token_address)
|
||||
),
|
||||
BigInt::from_bytes_le(
|
||||
if delta.sign { Sign::Plus } else { Sign::Minus },
|
||||
delta.amount.as_slice(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
235
substreams/ethereum-uniswap-v3/src/modules/5_map_pool_events.rs
Normal file
235
substreams/ethereum-uniswap-v3/src/modules/5_map_pool_events.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use std::{collections::HashMap, usize, vec};
|
||||
use substreams::store::{StoreGet, StoreGetBigInt, StoreGetProto};
|
||||
use substreams_ethereum::pb::eth::v2::{self as eth, TransactionTrace};
|
||||
|
||||
use substreams_helper::hex::Hexable;
|
||||
|
||||
use crate::{
|
||||
events::{get_log_changed_attributes, get_log_changed_balances},
|
||||
pb::{
|
||||
tycho::evm::v1::{BalanceChange, Block, BlockChanges, EntityChanges, TransactionChanges},
|
||||
uniswap::v3::Pool,
|
||||
},
|
||||
};
|
||||
|
||||
#[substreams::handlers::map]
|
||||
pub fn map_pool_events(
|
||||
block: eth::Block,
|
||||
created_pools: BlockChanges,
|
||||
pools_store: StoreGetProto<Pool>,
|
||||
balance_store: StoreGetBigInt,
|
||||
) -> Result<BlockChanges, substreams::errors::Error> {
|
||||
let mut tx_changes_map: HashMap<Vec<u8>, TransactionChanges> = HashMap::new();
|
||||
|
||||
// Add created pools to the tx_changes_map
|
||||
for change in created_pools.changes.into_iter() {
|
||||
let transaction = change.tx.as_ref().unwrap();
|
||||
tx_changes_map
|
||||
.entry(transaction.hash.clone())
|
||||
.and_modify(|c| {
|
||||
c.component_changes
|
||||
.extend(change.component_changes.clone());
|
||||
c.entity_changes
|
||||
.extend(change.entity_changes.clone());
|
||||
})
|
||||
.or_insert(change);
|
||||
}
|
||||
|
||||
for trx in block.transactions() {
|
||||
for (log, call_view) in trx.logs_with_calls() {
|
||||
// Skip if the log is not from a known uniswapV3 pool.
|
||||
if let Some(pool) =
|
||||
pools_store.get_last(format!("{}:{}", "Pool", &log.address.to_hex()))
|
||||
{
|
||||
let changed_attributes = get_log_changed_attributes(
|
||||
log,
|
||||
&call_view.call.storage_changes,
|
||||
pool.address
|
||||
.clone()
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("Pool address is not 20 bytes long"),
|
||||
);
|
||||
|
||||
let mut balance_changes: Vec<BalanceChange> = vec![];
|
||||
|
||||
if !(get_log_changed_balances(log, &pool).is_empty()) {
|
||||
let token_0_balance = balance_store.get_last(format!(
|
||||
"pool:{0}:token:{1}",
|
||||
hex::encode(&pool.address),
|
||||
hex::encode(&pool.token0)
|
||||
));
|
||||
let token_1_balance = balance_store.get_last(format!(
|
||||
"pool:{0}:token:{1}",
|
||||
hex::encode(&pool.address),
|
||||
hex::encode(&pool.token1)
|
||||
));
|
||||
|
||||
let pool_address_utf8 = pool
|
||||
.address
|
||||
.clone()
|
||||
.to_hex()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
|
||||
let token_0_balance_change = BalanceChange {
|
||||
component_id: pool_address_utf8.clone(),
|
||||
token: pool.token0.clone(),
|
||||
balance: token_0_balance
|
||||
.clone()
|
||||
.expect("Couldn't get balance from store")
|
||||
.to_bytes_be()
|
||||
.1,
|
||||
};
|
||||
let token_1_balance_change = BalanceChange {
|
||||
component_id: pool_address_utf8.clone(),
|
||||
token: pool.token1.clone(),
|
||||
balance: token_1_balance
|
||||
.clone()
|
||||
.expect("Couldn't get balance from store")
|
||||
.to_bytes_be()
|
||||
.1,
|
||||
};
|
||||
|
||||
balance_changes.extend([token_0_balance_change, token_1_balance_change]);
|
||||
}
|
||||
|
||||
// Create entity changes
|
||||
let entity_changes: Vec<EntityChanges> = vec![EntityChanges {
|
||||
component_id: pool.address.clone().to_hex(),
|
||||
attributes: changed_attributes,
|
||||
}];
|
||||
|
||||
update_tx_changes_map(entity_changes, balance_changes, &mut tx_changes_map, trx);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make a list of all HashMap values:
|
||||
let tx_entity_changes: Vec<TransactionChanges> = tx_changes_map.into_values().collect();
|
||||
|
||||
let tycho_block: Block = block.into();
|
||||
|
||||
let block_entity_changes =
|
||||
BlockChanges { block: Some(tycho_block), changes: tx_entity_changes };
|
||||
|
||||
Ok(block_entity_changes)
|
||||
}
|
||||
|
||||
fn update_tx_changes_map(
|
||||
entity_changes: Vec<EntityChanges>,
|
||||
balance_changes: Vec<BalanceChange>,
|
||||
tx_changes_map: &mut HashMap<Vec<u8>, TransactionChanges>,
|
||||
tx_trace: &TransactionTrace,
|
||||
) {
|
||||
// Get the tx hash
|
||||
let tx_hash = tx_trace.hash.clone();
|
||||
|
||||
// Get the tx changes from the map
|
||||
let tx_changes = tx_changes_map.get_mut(&tx_hash);
|
||||
|
||||
// Update the tx changes
|
||||
if let Some(tx_changes) = tx_changes {
|
||||
// Merge the entity changes
|
||||
tx_changes.entity_changes =
|
||||
merge_entity_changes(&tx_changes.entity_changes, &entity_changes);
|
||||
|
||||
// Merge the balance changes
|
||||
tx_changes.balance_changes =
|
||||
merge_balance_changes(&tx_changes.balance_changes, &balance_changes);
|
||||
} else {
|
||||
// If the tx is not in the map, add it
|
||||
let tx_changes = TransactionChanges {
|
||||
tx: Some(tx_trace.into()),
|
||||
contract_changes: vec![],
|
||||
entity_changes,
|
||||
balance_changes,
|
||||
component_changes: vec![],
|
||||
};
|
||||
tx_changes_map.insert(tx_hash, tx_changes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges new entity changes into an existing collection of entity changes and returns the merged
|
||||
/// result. For each entity change, if an entity change with the same component_id exists, its
|
||||
/// attributes are merged. If an attribute with the same name exists, the new attribute replaces the
|
||||
/// old one.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `existing_changes`: A reference to a vector of existing entity changes.
|
||||
/// - `new_changes`: A reference to a vector of new entity changes to be merged.
|
||||
///
|
||||
/// Returns:
|
||||
/// A new `Vec<EntityChanges>` containing the merged entity changes.
|
||||
fn merge_entity_changes(
|
||||
existing_changes: &[EntityChanges],
|
||||
new_changes: &Vec<EntityChanges>,
|
||||
) -> Vec<EntityChanges> {
|
||||
let mut changes_map = existing_changes
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|change| (change.component_id.clone(), change))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for change in new_changes {
|
||||
match changes_map.get_mut(&change.component_id) {
|
||||
Some(existing_change) => {
|
||||
let mut attributes_map = existing_change
|
||||
.attributes
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|attr| (attr.name.clone(), attr))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for attr in &change.attributes {
|
||||
attributes_map.insert(attr.name.clone(), attr.clone());
|
||||
}
|
||||
|
||||
existing_change.attributes = attributes_map.into_values().collect();
|
||||
}
|
||||
None => {
|
||||
changes_map.insert(change.component_id.clone(), change.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes_map.into_values().collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
struct BalanceChangeKey {
|
||||
token: Vec<u8>,
|
||||
component_id: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Merges two vectors of `BalanceChange` structures into a single vector. If two `BalanceChange`
|
||||
/// instances have the same combination of `token` and `component_id`, the value from the
|
||||
/// `new_entries` vector will replace the one from the `current` vector.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `current`: A reference to a vector of `BalanceChange` instances representing the current
|
||||
/// balance changes.
|
||||
/// - `new_entries`: A reference to a vector of `BalanceChange` instances representing new balance
|
||||
/// changes to be merged.
|
||||
///
|
||||
/// Returns:
|
||||
/// A `Vec<BalanceChange>` that contains the merged balance changes.
|
||||
fn merge_balance_changes(
|
||||
current: &[BalanceChange],
|
||||
new_entries: &Vec<BalanceChange>,
|
||||
) -> Vec<BalanceChange> {
|
||||
let mut balances = HashMap::new();
|
||||
|
||||
for balance_change in current.iter().chain(new_entries) {
|
||||
let key = BalanceChangeKey {
|
||||
token: balance_change.token.clone(),
|
||||
component_id: balance_change.component_id.clone(),
|
||||
};
|
||||
|
||||
balances.insert(key, balance_change.clone());
|
||||
}
|
||||
|
||||
balances.into_values().collect()
|
||||
}
|
||||
18
substreams/ethereum-uniswap-v3/src/modules/mod.rs
Normal file
18
substreams/ethereum-uniswap-v3/src/modules/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
pub use map_pool_created::map_pools_created;
|
||||
pub use map_pool_events::map_pool_events;
|
||||
pub use store_pools::store_pools;
|
||||
|
||||
#[path = "1_map_pool_created.rs"]
|
||||
mod map_pool_created;
|
||||
|
||||
#[path = "2_store_pools.rs"]
|
||||
mod store_pools;
|
||||
|
||||
#[path = "3_map_balance_changes.rs"]
|
||||
mod map_balance_changes;
|
||||
|
||||
#[path = "4_store_pools_balances.rs"]
|
||||
mod store_pools_balances;
|
||||
|
||||
#[path = "5_map_pool_events.rs"]
|
||||
mod map_pool_events;
|
||||
Reference in New Issue
Block a user