Repository: gakonst/ethers-rs
Pull request #1503 (2022-07-24 23:39:50)

feat(etherscan): erc1155 token transfer events endpoint

## Motivation

Etherscan supports querying 1155 token transfer events: https://docs.etherscan.io/api-endpoints/accounts#get-a-list-of-erc1155-token-transfer-events-by-address. However, this was missing from the etherscan client.

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

Added a function `get_erc1155_token_transfer_events` to query this. There is already the corresponding functions for ERC20 and ERC721.

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [x] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47862703..10851490 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Unreleased
 
+- Added `get_erc1155_token_transfer_events` function for etherscan client [#1503](https://github.com/gakonst/ethers-rs/pull/1503)
 - Add support for Geth `debug_traceTransaction` [#1469](https://github.com/gakonst/ethers-rs/pull/1469)
 - Use correct, new transaction type for `typool_content` RPC endpoint [#1501](https://github.com/gakonst/ethers-rs/pull/1501)
 - Fix the default config for generated `BuildInfo` [#1458](https://github.com/gakonst/ethers-rs/pull/1458)
diff --git a/ethers-core/src/abi/codec.rs b/ethers-core/src/abi/codec.rs
index 6a888aa0..a953eef0 100644
--- a/ethers-core/src/abi/codec.rs
+++ b/ethers-core/src/abi/codec.rs
@@ -167,7 +167,11 @@ impl_abi_codec_tuple!(12, A, B, C, D, E, F, G, H, I, J, K, L);
 impl_abi_codec_tuple!(13, A, B, C, D, E, F, G, H, I, J, K, L, M);
 impl_abi_codec_tuple!(14, A, B, C, D, E, F, G, H, I, J, K, L, M, N);
 impl_abi_codec_tuple!(15, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
-impl_abi_codec_tuple!(16, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
+impl_abi_codec_tuple!(16, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
+impl_abi_codec_tuple!(17, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
+impl_abi_codec_tuple!(18, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
+impl_abi_codec_tuple!(19, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
+impl_abi_codec_tuple!(20, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
 
 #[cfg(test)]
 mod tests {
diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs
index 4a332e6d..88c57f88 100644
--- a/ethers-core/src/abi/mod.rs
+++ b/ethers-core/src/abi/mod.rs
@@ -188,6 +188,11 @@ impl_abi_type_tuple!(13, A, B, C, D, E, F, G, H, I, J, K, L, M);
 impl_abi_type_tuple!(14, A, B, C, D, E, F, G, H, I, J, K, L, M, N);
 impl_abi_type_tuple!(15, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
 impl_abi_type_tuple!(16, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
+impl_abi_type_tuple!(17, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
+impl_abi_type_tuple!(18, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
+impl_abi_type_tuple!(19, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
+impl_abi_type_tuple!(20, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
+impl_abi_type_tuple!(21, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
 
 #[cfg(test)]
 mod tests {
diff --git a/ethers-core/src/abi/tokens.rs b/ethers-core/src/abi/tokens.rs
index e0684bb0..8839d14d 100644
--- a/ethers-core/src/abi/tokens.rs
+++ b/ethers-core/src/abi/tokens.rs
@@ -123,6 +123,11 @@ impl_tuples!(13, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M
 impl_tuples!(14, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, );
 impl_tuples!(15, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, );
 impl_tuples!(16, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, );
+impl_tuples!(17, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, Q:16,);
+impl_tuples!(18, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, Q:16, R:17,);
+impl_tuples!(19, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, Q:16, R:17, S:18,);
+impl_tuples!(20, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, Q:16, R:17, S:18, T:19,);
+impl_tuples!(21, A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, Q:16, R:17, S:18, T:19, U:20,);
 
 impl Tokenizable for Token {
     fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
@@ -329,6 +334,11 @@ impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M,);
 impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N,);
 impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O,);
 impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P,);
+impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q,);
+impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R,);
+impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S,);
+impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T,);
+impl_tokenizable_item_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U,);
 
 impl Tokenizable for Vec<u8> {
     fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
diff --git a/ethers-etherscan/src/account.rs b/ethers-etherscan/src/account.rs
index cc310bd8..7f01c578 100644
--- a/ethers-etherscan/src/account.rs
+++ b/ethers-etherscan/src/account.rs
@@ -203,6 +203,33 @@ pub struct ERC721TokenTransferEvent {
     pub confirmations: U64,
 }
 
+/// The raw response from the ERC1155 transfer list API endpoint
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ERC1155TokenTransferEvent {
+    pub block_number: BlockNumber,
+    pub time_stamp: String,
+    pub hash: H256,
+    pub nonce: U256,
+    pub block_hash: H256,
+    pub from: Address,
+    pub contract_address: Address,
+    pub to: Option<Address>,
+    #[serde(rename = "tokenID")]
+    pub token_id: String,
+    pub token_value: String,
+    pub token_name: String,
+    pub token_symbol: String,
+    pub transaction_index: U64,
+    pub gas: U256,
+    pub gas_price: Option<U256>,
+    pub gas_used: U256,
+    pub cumulative_gas_used: U256,
+    /// deprecated
+    pub input: String,
+    pub confirmations: U64,
+}
+
 /// The raw response from the mined blocks API endpoint
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -532,6 +559,36 @@ impl Client {
         Ok(response.result)
     }
 
+    /// Returns the list of ERC-1155 ( NFT ) tokens transferred by an address, with optional
+    /// filtering by token contract.
+    ///
+    /// ```no_run
+    /// # use ethers_etherscan::{Client, account::TokenQueryOption};
+    /// # use ethers_core::types::Chain;
+    ///
+    /// # #[tokio::main]
+    /// # async fn main() {
+    ///     let client = Client::new(Chain::Mainnet, "API_KEY").unwrap();
+    ///     let txs = client
+    ///         .get_erc1155_token_transfer_events(
+    ///             TokenQueryOption::ByAddressAndContract(
+    ///                 "0x216CD350a4044e7016f14936663e2880Dd2A39d7".parse().unwrap(),
+    ///                 "0x495f947276749ce646f68ac8c248420045cb7b5e".parse().unwrap(),
+    ///          ), None).await.unwrap();
+    /// # }
+    /// ```
+    pub async fn get_erc1155_token_transfer_events(
+        &self,
+        event_query_option: TokenQueryOption,
+        params: Option<TxListParams>,
+    ) -> Result<Vec<ERC1155TokenTransferEvent>> {
+        let params = event_query_option.into_params(params.unwrap_or_default());
+        let query = self.create_query("account", "token1155tx", params);
+        let response: Response<Vec<ERC1155TokenTransferEvent>> = self.get_json(&query).await?;
+
+        Ok(response.result)
+    }
+
     /// Returns the list of blocks mined by an address.
     ///
     /// ```no_run
@@ -708,6 +765,26 @@ mod tests {
         .await
     }
 
+    #[tokio::test]
+    #[serial]
+    async fn get_erc1155_transfer_events_success() {
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Mainnet).unwrap();
+
+            let txs = client
+                .get_erc1155_token_transfer_events(
+                    TokenQueryOption::ByAddressAndContract(
+                        "0x216CD350a4044e7016f14936663e2880Dd2A39d7".parse().unwrap(),
+                        "0x495f947276749ce646f68ac8c248420045cb7b5e".parse().unwrap(),
+                    ),
+                    None,
+                )
+                .await;
+            assert!(txs.is_ok());
+        })
+        .await
+    }
+
     #[tokio::test]
     #[serial]
     async fn get_mined_blocks_success() {
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index d9005879..147bcbba 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -1619,6 +1619,7 @@ pub struct Ewasm {
     pub wasm: String,
 }
 
+/// Represents the `storage-layout` section of the `CompilerOutput` if selected.
 #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
 pub struct StorageLayout {
     pub storage: Vec<Storage>,
@@ -1647,9 +1648,16 @@ pub struct Storage {
 #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
 pub struct StorageType {
     pub encoding: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub key: Option<String>,
     pub label: String,
     #[serde(rename = "numberOfBytes")]
     pub number_of_bytes: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub value: Option<String>,
+    /// additional fields
+    #[serde(flatten)]
+    pub other: BTreeMap<String, serde_json::Value>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
@@ -2120,4 +2128,11 @@ mod tests {
         let value = serde_json::to_string(&c).unwrap();
         pretty_assertions::assert_eq!(s, value);
     }
+
+    #[test]
+    fn test_lossless_storage_layout() {
+        let input = include_str!("../../test-data/foundryissue2462.json");
+        let layout: StorageLayout = serde_json::from_str(input).unwrap();
+        pretty_assertions::assert_eq!(input, &serde_json::to_string_pretty(&layout).unwrap());
+    }
 }
diff --git a/examples/geth_trace_transaction.rs b/examples/geth_trace_transaction.rs
index e76266e3..dac3c62f 100644
--- a/examples/geth_trace_transaction.rs
+++ b/examples/geth_trace_transaction.rs
@@ -1,16 +1,19 @@
 use ethers::prelude::*;
 use eyre::Result;
-use std::{env, str::FromStr};
+use std::str::FromStr;
 
+/// use `debug_traceTransaction` to fetch traces
+/// requires, a valid endpoint in `RPC_URL` env var that supports `debug_traceTransaction`
 #[tokio::main]
 async fn main() -> Result<()> {
-    let rpc_url: String = env::var("RPC_URL")?;
-    let client = Provider::<Http>::try_from(rpc_url)?;
-    let tx_hash = "0x97a02abf405d36939e5b232a5d4ef5206980c5a6661845436058f30600c52df7";
-    let h: H256 = H256::from_str(tx_hash)?;
-    let options: GethDebugTracingOptions = GethDebugTracingOptions::default();
-    let traces = client.debug_trace_transaction(h, options).await?;
-    println!("{:?}", traces);
+    if let Ok(url) = std::env::var("RPC_URL") {
+        let client = Provider::<Http>::try_from(url)?;
+        let tx_hash = "0x97a02abf405d36939e5b232a5d4ef5206980c5a6661845436058f30600c52df7";
+        let h: H256 = H256::from_str(tx_hash)?;
+        let options: GethDebugTracingOptions = GethDebugTracingOptions::default();
+        let traces = client.debug_trace_transaction(h, options).await?;
+        println!("{:?}", traces);
+    }
 
     Ok(())
 }


Test patch:
diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs
index 8256dc3a..fd661ad0 100644
--- a/ethers-contract/tests/it/abigen.rs
+++ b/ethers-contract/tests/it/abigen.rs
@@ -654,3 +654,8 @@ fn can_generate_to_string_overload() {
         ToStringCalls::ToString5(_) => {}
     };
 }
+
+#[test]
+fn can_generate_large_event() {
+    abigen!(NewSale, "ethers-contract/tests/solidity-contracts/sale.json");
+}
diff --git a/ethers-contract/tests/solidity-contracts/sale.json b/ethers-contract/tests/solidity-contracts/sale.json
new file mode 100644
index 00000000..d1192e30
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/sale.json
@@ -0,0 +1,81 @@
+[
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "marketplaceId",
+        "type": "uint256"
+      },
+      {
+        "indexed": false,
+        "internalType": "address[]",
+        "name": "nfts",
+        "type": "address[]"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256[]",
+        "name": "tokenIds",
+        "type": "uint256[]"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256[]",
+        "name": "amountBatches",
+        "type": "uint256[]"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint128",
+        "name": "price",
+        "type": "uint128"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "duration",
+        "type": "uint256"
+      },
+      {
+        "indexed": false,
+        "internalType": "bool",
+        "name": "isAuction",
+        "type": "bool"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      },
+      {
+        "indexed": false,
+        "internalType": "bool",
+        "name": "isNSFW",
+        "type": "bool"
+      },
+      {
+        "indexed": false,
+        "internalType": "string",
+        "name": "marketplaceURI",
+        "type": "string"
+      },
+      {
+        "indexed": false,
+        "internalType": "string",
+        "name": "searchKeywords",
+        "type": "string"
+      },
+      {
+        "indexed": false,
+        "internalType": "address[]",
+        "name": "addresses",
+        "type": "address[]"
+      }
+    ],
+    "name": "NewSale",
+    "type": "event"
+  }
+]
\ No newline at end of file
diff --git a/ethers-solc/test-data/foundryissue2462.json b/ethers-solc/test-data/foundryissue2462.json
new file mode 100644
index 00000000..ea9764ae
--- /dev/null
+++ b/ethers-solc/test-data/foundryissue2462.json
@@ -0,0 +1,54 @@
+{
+  "storage": [
+    {
+      "astId": 3,
+      "contract": "test-data/foundryissue2462.sol:Token",
+      "label": "x",
+      "offset": 0,
+      "slot": "0",
+      "type": "t_uint256"
+    },
+    {
+      "astId": 7,
+      "contract": "test-data/foundryissue2462.sol:Token",
+      "label": "balances",
+      "offset": 0,
+      "slot": "1",
+      "type": "t_mapping(t_address,t_uint256)"
+    },
+    {
+      "astId": 13,
+      "contract": "test-data/foundryissue2462.sol:Token",
+      "label": "allowances",
+      "offset": 0,
+      "slot": "2",
+      "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))"
+    }
+  ],
+  "types": {
+    "t_address": {
+      "encoding": "inplace",
+      "label": "address",
+      "numberOfBytes": "20"
+    },
+    "t_mapping(t_address,t_mapping(t_address,t_uint256))": {
+      "encoding": "mapping",
+      "key": "t_address",
+      "label": "mapping(address => mapping(address => uint256))",
+      "numberOfBytes": "32",
+      "value": "t_mapping(t_address,t_uint256)"
+    },
+    "t_mapping(t_address,t_uint256)": {
+      "encoding": "mapping",
+      "key": "t_address",
+      "label": "mapping(address => uint256)",
+      "numberOfBytes": "32",
+      "value": "t_uint256"
+    },
+    "t_uint256": {
+      "encoding": "inplace",
+      "label": "uint256",
+      "numberOfBytes": "32"
+    }
+  }
+}
\ No newline at end of file