From 25733676f2b47c9fcad04d9326c6a81f8ac7c0e5 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Thu, 30 May 2024 14:38:25 +0200 Subject: [PATCH] Add `subscribe_all_logs` example (#84) * add subscribe_all_logs example * use getter, prebuild examples for speedup * fix clippy * remote optimization, causes issues with rapid anvil spawning * safely handle getting topic0 * fix clippy * remove api key * use topic0, update to latest version of Alloy * clean up lint rules --- .github/workflows/integration.yml | 1 + .github/workflows/test.yml | 1 + CONTRIBUTING.md | 1 + Cargo.toml | 19 +- README.md | 3 +- .../examples/abi/{IERC20.json => IWETH9.json} | 0 .../contracts/examples/interact_with_abi.rs | 8 +- examples/queries/examples/query_logs.rs | 6 +- .../subscriptions/examples/abi/IWETH9.json | 279 ++++++++++++++++++ .../examples/subscribe_all_logs.rs | 65 ++++ .../subscriptions/examples/subscribe_logs.rs | 12 +- .../examples/trace_transaction.rs | 5 +- 12 files changed, 373 insertions(+), 27 deletions(-) rename examples/contracts/examples/abi/{IERC20.json => IWETH9.json} (100%) create mode 100644 examples/subscriptions/examples/abi/IWETH9.json create mode 100644 examples/subscriptions/examples/subscribe_all_logs.rs diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index d9017a96..987e126d 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -35,6 +35,7 @@ jobs: -e 'ws' \ -e 'ws_auth' \ -e 'subscribe_logs' \ + -e 'subscribe_all_logs' \ -e 'trace_call' \ -e 'trace_transaction' \ | xargs -n1 echo diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a09324d7..87081a05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,7 @@ jobs: -e 'ws' \ -e 'ws_auth' \ -e 'subscribe_logs' \ + -e 'subscribe_all_logs' \ -e 'trace_call' \ -e 'trace_transaction' \ | xargs -n1 echo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abe3aab0..c88c8c9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,6 +154,7 @@ cargo run --example 2>&1 \ -e 'ws' \ -e 'ws_auth' \ -e 'subscribe_logs' \ + -e 'subscribe_all_logs' \ -e 'trace_call' \ -e 'trace_transaction' \ | xargs -I {} sh -c 'if cargo run --example {} --quiet 1>/dev/null; then \ diff --git a/Cargo.toml b/Cargo.toml index 0d137bd0..3cabb2d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,9 @@ exclude = ["examples/"] [workspace.lints] rust.missing_debug_implementations = "warn" rust.missing_docs = "warn" +rust.rust_2018_idioms = { level = "deny", priority = -1 } rust.unreachable_pub = "warn" rust.unused_must_use = "deny" -rust.rust_2018_idioms = { level = "deny", priority = -1 } rustdoc.all = "warn" [workspace.lints.clippy] @@ -37,6 +37,7 @@ iter_on_empty_collections = "warn" iter_with_drain = "warn" large_stack_frames = "warn" manual_clamp = "warn" +missing_const_for_fn = "warn" mutex_integer = "warn" needless_pass_by_ref_mut = "warn" nonstandard_macro_braces = "warn" @@ -54,8 +55,8 @@ tuple_array_conversions = "warn" uninhabited_references = "warn" unused_peekable = "warn" unused_rounding = "warn" +use_self = "warn" useless_let_if_seq = "warn" -uninlined_format_args = "warn" # These are nursery lints which have findings. Allow them for now. Some are not # quite mature enough for use in our codebase and some we don't really want. @@ -68,7 +69,6 @@ empty_line_after_doc_comments = "allow" fallible_impl_from = "allow" future_not_send = "allow" iter_on_single_items = "allow" -missing_const_for_fn = "allow" needless_collect = "allow" non_send_fields_in_send_ty = "allow" option_if_let_else = "allow" @@ -78,26 +78,24 @@ significant_drop_tightening = "allow" string_lit_as_bytes = "allow" type_repetition_in_bounds = "allow" unnecessary_struct_initialization = "allow" -use_self = "allow" [workspace.dependencies] -alloy = { git = "https://github.com/alloy-rs/alloy", rev = "bd39117", features = [ +alloy = { git = "https://github.com/alloy-rs/alloy", rev = "3edca45", features = [ "consensus", - "kzg", - "eips", "contract", + "eips", + "kzg", "network", "node-bindings", - "providers", "provider-http", "provider-ipc", "provider-ws", - "rpc-client", + "providers", "rpc-client-ipc", "rpc-client-ws", + "rpc-client", "rpc-types-eth", "rpc-types-trace", - "signers", "signer-aws", "signer-keystore", "signer-ledger", @@ -105,6 +103,7 @@ alloy = { git = "https://github.com/alloy-rs/alloy", rev = "bd39117", features = "signer-trezor", "signer-wallet", "signer-yubihsm", + "signers", ] } # async diff --git a/README.md b/README.md index ac86fdf7..4508c30c 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ This repository contains the following examples: - [x] Subscriptions - [x] [Subscribe and watch blocks](./examples/subscriptions/examples/subscribe_blocks.rs) - [x] [Watch and poll for contract event logs](./examples/subscriptions/examples/poll_logs.rs) - - [x] [Subscribe and listen for contract event logs](./examples/subscriptions/examples/subscribe_logs.rs) + - [x] [Subscribe and listen for specific contract event logs](./examples/subscriptions/examples/subscribe_logs.rs) + - [x] [Subscribe and listen for all contract event logs](./examples/subscriptions/examples/subscribe_all_logs.rs) - [x] [Event multiplexer](./examples/subscriptions/examples/event_multiplexer.rs) - [x] Providers - [x] [Builder](./examples/providers/examples/builder.rs) diff --git a/examples/contracts/examples/abi/IERC20.json b/examples/contracts/examples/abi/IWETH9.json similarity index 100% rename from examples/contracts/examples/abi/IERC20.json rename to examples/contracts/examples/abi/IWETH9.json diff --git a/examples/contracts/examples/interact_with_abi.rs b/examples/contracts/examples/interact_with_abi.rs index ca487745..3581c13b 100644 --- a/examples/contracts/examples/interact_with_abi.rs +++ b/examples/contracts/examples/interact_with_abi.rs @@ -7,8 +7,8 @@ use eyre::Result; sol!( #[allow(missing_docs)] #[sol(rpc)] - IERC20, - "examples/abi/IERC20.json" + IWETH9, + "examples/abi/IWETH9.json" ); #[tokio::main] @@ -22,10 +22,10 @@ async fn main() -> Result<()> { let provider = ProviderBuilder::new().on_http(rpc_url); // Create a contract instance. - let contract = IERC20::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse()?, provider); + let contract = IWETH9::new("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse()?, provider); // Call the contract, retrieve the total supply. - let IERC20::totalSupplyReturn { _0 } = contract.totalSupply().call().await?; + let IWETH9::totalSupplyReturn { _0 } = contract.totalSupply().call().await?; println!("WETH total supply is {_0}"); diff --git a/examples/queries/examples/query_logs.rs b/examples/queries/examples/query_logs.rs index eebfcf2a..8f3bcdcd 100644 --- a/examples/queries/examples/query_logs.rs +++ b/examples/queries/examples/query_logs.rs @@ -27,9 +27,9 @@ async fn main() -> Result<()> { } // Get all logs from the latest block that match the transfer event signature/topic. - let tranfer_event_signature = + let transfer_event_signature = b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); - let filter = Filter::new().event_signature(tranfer_event_signature).from_block(latest_block); + let filter = Filter::new().event_signature(transfer_event_signature).from_block(latest_block); // You could also use the event name instead of the event signature like so: // .event("Transfer(address,address,uint256)") @@ -40,7 +40,7 @@ async fn main() -> Result<()> { println!("Transfer event: {log:?}"); } - // Get all from the latest block emitted by the UNI token address. + // Get all logs from the latest block emitted by the UNI token address. let uniswap_token_address = address!("1f9840a85d5aF5bf1D1762F925BDADdC4201F984"); let filter = Filter::new().address(uniswap_token_address).from_block(latest_block); diff --git a/examples/subscriptions/examples/abi/IWETH9.json b/examples/subscriptions/examples/abi/IWETH9.json new file mode 100644 index 00000000..37f31f97 --- /dev/null +++ b/examples/subscriptions/examples/abi/IWETH9.json @@ -0,0 +1,279 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "guy", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "src", + "type": "address" + }, + { + "name": "dst", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "dst", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "deposit", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": true, + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": true, + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + } +] \ No newline at end of file diff --git a/examples/subscriptions/examples/subscribe_all_logs.rs b/examples/subscriptions/examples/subscribe_all_logs.rs new file mode 100644 index 00000000..66e9f72b --- /dev/null +++ b/examples/subscriptions/examples/subscribe_all_logs.rs @@ -0,0 +1,65 @@ +//! Example of subscribing and listening for all contract events by WebSocket subscription. + +use alloy::{ + primitives::address, + providers::{Provider, ProviderBuilder}, + rpc::{ + client::WsConnect, + types::eth::{BlockNumberOrTag, Filter}, + }, + sol, + sol_types::SolEvent, +}; +use eyre::Result; +use futures_util::stream::StreamExt; + +// Codegen from ABI file to interact with the contract. +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + IWETH9, + "examples/abi/IWETH9.json" +); + +#[tokio::main] +async fn main() -> Result<()> { + // Set up the WS transport which is consumed by the RPC client. + let rpc_url = "wss://eth-mainnet.g.alchemy.com/v2/your-api-key"; + + // Create the provider. + let ws = WsConnect::new(rpc_url); + let provider = ProviderBuilder::new().on_ws(ws).await?; + + // Create a filter to watch for all WETH9 events. + let weth9_token_address = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + let filter = Filter::new() + // By NOT specifying an `event` or `event_signature` we listen to ALL events of the + // contract. + .address(weth9_token_address) + .from_block(BlockNumberOrTag::Latest); + + // Subscribe to logs. + let sub = provider.subscribe_logs(&filter).await?; + let mut stream = sub.into_stream(); + + while let Some(log) = stream.next().await { + // Match on topic 0, the hash of the signature of the event. + match log.topic0() { + // Match the `Approval(address,address,uint256)` event. + Some(&IWETH9::Approval::SIGNATURE_HASH) => { + let IWETH9::Approval { src, guy, wad } = log.log_decode()?.inner.data; + println!("Approval from {src} to {guy} of value {wad}"); + } + // Match the `Transfer(address,address,uint256)` event. + Some(&IWETH9::Transfer::SIGNATURE_HASH) => { + let IWETH9::Transfer { src, dst, wad } = log.log_decode()?.inner.data; + println!("Transfer from {src} to {dst} of value {wad}"); + } + // WETH9's `Deposit(address,uint256)` and `Withdrawal(address,uint256)` events are not + // handled here. + _ => (), + } + } + + Ok(()) +} diff --git a/examples/subscriptions/examples/subscribe_logs.rs b/examples/subscriptions/examples/subscribe_logs.rs index cf03a348..2abbc940 100644 --- a/examples/subscriptions/examples/subscribe_logs.rs +++ b/examples/subscriptions/examples/subscribe_logs.rs @@ -1,7 +1,7 @@ -//! Example of subscribing and listening for contract events by WebSocket subscription. +//! Example of subscribing and listening for specific contract events by WebSocket subscription. use alloy::{ - primitives::{address, b256}, + primitives::address, providers::{Provider, ProviderBuilder}, rpc::{ client::WsConnect, @@ -20,13 +20,13 @@ async fn main() -> Result<()> { let ws = WsConnect::new(rpc_url); let provider = ProviderBuilder::new().on_ws(ws).await?; - // Create a filter to watch for Uniswap token transfers. + // Create a filter to watch for UNI token transfers. let uniswap_token_address = address!("1f9840a85d5aF5bf1D1762F925BDADdC4201F984"); - let tranfer_event_signature = - b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); let filter = Filter::new() .address(uniswap_token_address) - .event_signature(tranfer_event_signature) + // By specifying an `event` or `event_signature` we listen for a specific event of the + // contract. In this case the `Transfer(address,address,uint256)` event. + .event("Transfer(address,address,uint256)") .from_block(BlockNumberOrTag::Latest); // Subscribe to logs. diff --git a/examples/transactions/examples/trace_transaction.rs b/examples/transactions/examples/trace_transaction.rs index 475fa665..6a0ff1ef 100644 --- a/examples/transactions/examples/trace_transaction.rs +++ b/examples/transactions/examples/trace_transaction.rs @@ -21,10 +21,10 @@ async fn main() -> Result<()> { let rpc_url = anvil.endpoint().parse()?; let provider = ProviderBuilder::new().on_http(rpc_url); - // Hash of the tx we want to trace + // Hash of the tx we want to trace. let hash = b256!("97a02abf405d36939e5b232a5d4ef5206980c5a6661845436058f30600c52df7"); - // Default tracing + // Trace with the default tracer. let default_options = GethDebugTracingOptions::default(); let result = provider.debug_trace_transaction(hash, default_options).await?; @@ -49,7 +49,6 @@ async fn main() -> Result<()> { tracer: Some(GethDebugTracerType::JsTracer("{data: [], fault: function(log) {}, step: function(log) { if(log.op.toString() == \"DELEGATECALL\") this.data.push(log.stack.peek(0)); }, result: function() { return this.data; }}".into())), ..Default::default() }; - let result = provider.debug_trace_transaction(hash, js_options).await?; println!("JS_TRACER: {result:?}");