Simplify async code in in_instructions_unordered

Outsources fetching the ERC20 events to top_level_transfers_unordered.
This commit is contained in:
Luke Parker
2025-01-24 05:34:49 -05:00
parent 201b675031
commit f948881eba
6 changed files with 284 additions and 324 deletions

View File

@@ -2,8 +2,7 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use core::borrow::Borrow;
use std::{sync::Arc, collections::HashMap};
use std::collections::HashMap;
use alloy_core::primitives::{Address, U256};
@@ -57,20 +56,27 @@ pub struct TopLevelTransfer {
pub data: Vec<u8>,
}
/// The result of `Erc20::top_level_transfers_unordered`.
pub struct TopLevelTransfers {
/// Every `Transfer` log of the contextual ERC20 to the contextual account, indexed by
/// their transaction.
///
/// The ERC20/account is labelled contextual as it isn't directly named here. Instead, they're
/// assumed contextual to how this was created.
pub logs: HashMap<[u8; 32], Vec<Log>>,
/// All of the top-level transfers of the contextual ERC20 to the contextual account.
///
/// The ERC20/account is labelled contextual as it isn't directly named here. Instead, they're
/// assumed contextual to how this was created.
pub transfers: Vec<TopLevelTransfer>,
}
/// A view for an ERC20 contract.
#[derive(Clone, Debug)]
pub struct Erc20 {
provider: Arc<RootProvider<SimpleRequest>>,
address: Address,
}
pub struct Erc20;
impl Erc20 {
/// Construct a new view of the specified ERC20 contract.
pub fn new(provider: Arc<RootProvider<SimpleRequest>>, address: Address) -> Self {
Self { provider, address }
}
/// The filter for transfer logs of the specified ERC20, to the specified recipient.
pub fn transfer_filter(from_block: u64, to_block: u64, erc20: Address, to: Address) -> Filter {
fn transfer_filter(from_block: u64, to_block: u64, erc20: Address, to: Address) -> Filter {
let filter = Filter::new().from_block(from_block).to_block(to_block);
filter.address(erc20).event_signature(Transfer::SIGNATURE_HASH).topic2(to.into_word())
}
@@ -78,32 +84,35 @@ impl Erc20 {
/// Yield the top-level transfer for the specified transaction (if one exists).
///
/// The passed-in logs MUST be the logs for this transaction. The logs MUST be filtered to the
/// `Transfer` events of the intended token(s) and the intended `to` transferred to. These
/// `Transfer` events of the intended token and the intended `to` transferred to. These
/// properties are completely unchecked and assumed to be the case.
///
/// This does NOT yield THE top-level transfer. If multiple `Transfer` events have identical
/// structure to the top-level transfer call, the earliest `Transfer` event present in the logs
/// is considered the top-level transfer.
/// structure to the top-level transfer call, the first `Transfer` event present in the logs is
/// considered the top-level transfer.
// Yielding THE top-level transfer would require tracing the transaction execution and isn't
// worth the effort.
pub async fn top_level_transfer(
provider: impl AsRef<RootProvider<SimpleRequest>>,
async fn top_level_transfer(
provider: &RootProvider<SimpleRequest>,
erc20: Address,
transaction_hash: [u8; 32],
mut transfer_logs: Vec<impl Borrow<Log>>,
transfer_logs: &[Log],
) -> Result<Option<TopLevelTransfer>, RpcError<TransportErrorKind>> {
// Fetch the transaction
let transaction =
provider.as_ref().get_transaction_by_hash(transaction_hash.into()).await?.ok_or_else(
|| {
TransportErrorKind::Custom(
"node didn't have the transaction which emitted a log it had".to_string().into(),
)
},
)?;
provider.get_transaction_by_hash(transaction_hash.into()).await?.ok_or_else(|| {
TransportErrorKind::Custom(
"node didn't have the transaction which emitted a log it had".to_string().into(),
)
})?;
// If this transaction didn't call this ERC20 at a top-level, return
if transaction.inner.to() != Some(erc20) {
return Ok(None);
}
// If this is a top-level call...
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due
// to the `InInstruction` appended after the call itself
// to the additional data appended after the call itself
let Ok(call) = IERC20Calls::abi_decode(transaction.inner.input(), false) else {
return Ok(None);
};
@@ -116,21 +125,12 @@ impl Erc20 {
_ => return Ok(None),
};
// Sort the logs to ensure the the earliest logs are first
transfer_logs.sort_by_key(|log| log.borrow().log_index);
// Find the log for this top-level transfer
for log in transfer_logs {
// Check the log is for the called contract
// This handles the edge case where we're checking if transfers of token X were top-level and
// a transfer of token Y (with equivalent structure) was top-level
if Some(log.borrow().address()) != transaction.inner.to() {
continue;
}
// Since the caller is responsible for filtering these to `Transfer` events, we can assume
// this is a non-compliant ERC20 or an error with the logs fetched. We assume ERC20
// compliance here, making this an RPC error
let log = log.borrow().log_decode::<Transfer>().map_err(|_| {
let log = log.log_decode::<Transfer>().map_err(|_| {
TransportErrorKind::Custom("log didn't include a valid transfer event".to_string().into())
})?;
@@ -158,8 +158,8 @@ impl Erc20 {
) => Vec::from(inInstruction),
}
} else {
// We don't error here so this transfer is propagated up the stack, even without the
// InInstruction. In practice, Serai should acknowledge this and return it to the sender
// If there was no additional data appended, use an empty Vec (which has no data)
// This has a slight information loss in that it's None -> Some(vec![]), but it's fine
vec![]
};
@@ -177,69 +177,76 @@ impl Erc20 {
/// Fetch all top-level transfers to the specified address for this token.
///
/// The result of this function is unordered.
/// The `transfers` in the result are unordered. The `logs` are sorted by index.
pub async fn top_level_transfers_unordered(
&self,
provider: &RootProvider<SimpleRequest>,
from_block: u64,
to_block: u64,
erc20: Address,
to: Address,
) -> Result<Vec<TopLevelTransfer>, RpcError<TransportErrorKind>> {
// Get all transfers within these blocks
let logs = self
.provider
.get_logs(&Self::transfer_filter(from_block, to_block, self.address, to))
.await?;
) -> Result<TopLevelTransfers, RpcError<TransportErrorKind>> {
let mut logs = {
// Get all transfers within these blocks
let logs = provider.get_logs(&Self::transfer_filter(from_block, to_block, erc20, to)).await?;
// The logs, indexed by their transactions
let mut transaction_logs = HashMap::new();
// Index the logs by their transactions
for log in logs {
// Double check the address which emitted this log
if log.address() != self.address {
Err(TransportErrorKind::Custom(
"node returned logs for a different address than requested".to_string().into(),
))?;
}
// Double check the event signature for this log
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
Err(TransportErrorKind::Custom(
"node returned a log for a different topic than filtered to".to_string().into(),
))?;
}
// Double check the `to` topic
if log.topics().get(2) != Some(&to.into_word()) {
Err(TransportErrorKind::Custom(
"node returned a transfer for a different `to` than filtered to".to_string().into(),
))?;
// The logs, indexed by their transactions
let mut transaction_logs = HashMap::new();
// Index the logs by their transactions
for log in logs {
// Double check the address which emitted this log
if log.address() != erc20 {
Err(TransportErrorKind::Custom(
"node returned logs for a different address than requested".to_string().into(),
))?;
}
// Double check the event signature for this log
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
Err(TransportErrorKind::Custom(
"node returned a log for a different topic than filtered to".to_string().into(),
))?;
}
// Double check the `to` topic
if log.topics().get(2) != Some(&to.into_word()) {
Err(TransportErrorKind::Custom(
"node returned a transfer for a different `to` than filtered to".to_string().into(),
))?;
}
let tx_id = log
.transaction_hash
.ok_or_else(|| {
TransportErrorKind::Custom("log didn't specify its transaction hash".to_string().into())
})?
.0;
transaction_logs.entry(tx_id).or_insert_with(|| Vec::with_capacity(1)).push(log);
}
let tx_id = log
.transaction_hash
.ok_or_else(|| {
TransportErrorKind::Custom("log didn't specify its transaction hash".to_string().into())
})?
.0;
transaction_logs
};
transaction_logs.entry(tx_id).or_insert_with(|| Vec::with_capacity(1)).push(log);
}
let mut transfers = vec![];
{
// Use `FuturesUnordered` so these RPC calls run in parallel
let mut futures = FuturesUnordered::new();
for (tx_id, transfer_logs) in &mut logs {
// Sort the logs to ensure the the earliest logs are first
transfer_logs.sort_by_key(|log| log.log_index);
futures.push(Self::top_level_transfer(provider, erc20, *tx_id, transfer_logs));
}
// Use `FuturesUnordered` so these RPC calls run in parallel
let mut futures = FuturesUnordered::new();
for (tx_id, transfer_logs) in transaction_logs {
futures.push(Self::top_level_transfer(&self.provider, tx_id, transfer_logs));
}
let mut top_level_transfers = vec![];
while let Some(top_level_transfer) = futures.next().await {
match top_level_transfer {
// Top-level transfer
Ok(Some(top_level_transfer)) => top_level_transfers.push(top_level_transfer),
// Not a top-level transfer
Ok(None) => continue,
// Failed to get this transaction's information so abort
Err(e) => Err(e)?,
while let Some(transfer) = futures.next().await {
match transfer {
// Top-level transfer
Ok(Some(transfer)) => transfers.push(transfer),
// Not a top-level transfer
Ok(None) => continue,
// Failed to get this transaction's information so abort
Err(e) => Err(e)?,
}
}
}
Ok(top_level_transfers)
Ok(TopLevelTransfers { logs, transfers })
}
}