From 3fab07c7710dfca5adee1f26c9aa49c0abbdbd7c Mon Sep 17 00:00:00 2001 From: Kent Theo Fourie <42357713+GundamDweeb@users.noreply.github.com> Date: Fri, 27 Oct 2023 23:38:25 +0200 Subject: [PATCH] add erc20 balances --- erc20-balances/.gitignore | 19 + erc20-balances/Cargo.toml | 29 + erc20-balances/LICENSE | 21 + erc20-balances/Makefile | 44 + erc20-balances/README.md | 159 +++ erc20-balances/abi/erc20.json | 222 +++ erc20-balances/build.rs | 9 + erc20-balances/package.json | 13 + erc20-balances/proto/v1/erc20.proto | 29 + erc20-balances/rust-toolchain.toml | 4 + erc20-balances/schema.graphql | 18 + erc20-balances/src/abi/erc20.rs | 1257 +++++++++++++++++ erc20-balances/src/abi/mod.rs | 2 + erc20-balances/src/lib.rs | 5 + erc20-balances/src/maps.rs | 314 ++++ erc20-balances/src/pb/erc20.types.v1.rs | 64 + erc20-balances/src/pb/mod.rs | 10 + .../pb/pinax.substreams.sink.prometheus.v1.rs | 276 ++++ .../src/pb/sf.substreams.sink.kv.v1.rs | 162 +++ erc20-balances/src/sinks.rs | 54 + erc20-balances/src/utils/helper.rs | 34 + erc20-balances/src/utils/mod.rs | 1 + erc20-balances/subgraph.yaml | 17 + erc20-balances/substreams.yaml | 49 + 24 files changed, 2812 insertions(+) create mode 100644 erc20-balances/.gitignore create mode 100644 erc20-balances/Cargo.toml create mode 100644 erc20-balances/LICENSE create mode 100644 erc20-balances/Makefile create mode 100644 erc20-balances/README.md create mode 100644 erc20-balances/abi/erc20.json create mode 100644 erc20-balances/build.rs create mode 100644 erc20-balances/package.json create mode 100644 erc20-balances/proto/v1/erc20.proto create mode 100644 erc20-balances/rust-toolchain.toml create mode 100644 erc20-balances/schema.graphql create mode 100644 erc20-balances/src/abi/erc20.rs create mode 100644 erc20-balances/src/abi/mod.rs create mode 100644 erc20-balances/src/lib.rs create mode 100644 erc20-balances/src/maps.rs create mode 100644 erc20-balances/src/pb/erc20.types.v1.rs create mode 100644 erc20-balances/src/pb/mod.rs create mode 100644 erc20-balances/src/pb/pinax.substreams.sink.prometheus.v1.rs create mode 100644 erc20-balances/src/pb/sf.substreams.sink.kv.v1.rs create mode 100644 erc20-balances/src/sinks.rs create mode 100644 erc20-balances/src/utils/helper.rs create mode 100644 erc20-balances/src/utils/mod.rs create mode 100644 erc20-balances/subgraph.yaml create mode 100644 erc20-balances/substreams.yaml diff --git a/erc20-balances/.gitignore b/erc20-balances/.gitignore new file mode 100644 index 0000000..a607a95 --- /dev/null +++ b/erc20-balances/.gitignore @@ -0,0 +1,19 @@ +build +node_modules +*.spkg +*.log + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/erc20-balances/Cargo.toml b/erc20-balances/Cargo.toml new file mode 100644 index 0000000..d81df96 --- /dev/null +++ b/erc20-balances/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "erc20" +version = "0.3.0" +description = "ERC-20" +edition = "2021" +authors = [ + "Denis ", + "Yaro ", + "Matthieu Vachon " +] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ethabi = "18.0" +prost = "0.11" +prost-types = "0.11" +num-bigint = "0.4" +substreams = "0.5" +substreams-ethereum = "0.9" +substreams-entity-change = "1.3" +substreams-sink-prometheus = "0.1" +substreams-sink-kv = "0.1" + +[build-dependencies] +prost-build = "0.11" +anyhow = "1" +substreams-ethereum = "0.9" diff --git a/erc20-balances/LICENSE b/erc20-balances/LICENSE new file mode 100644 index 0000000..3f7af7b --- /dev/null +++ b/erc20-balances/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Pinax + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/erc20-balances/Makefile b/erc20-balances/Makefile new file mode 100644 index 0000000..f6666e6 --- /dev/null +++ b/erc20-balances/Makefile @@ -0,0 +1,44 @@ +ENDPOINT ?= mainnet.eth.streamingfast.io:443 + +.PHONY: all +all: + make build + make pack + make graph + make info + +.PHONY: build +build: + cargo build --target wasm32-unknown-unknown --release + +.PHONY: protogen +protogen: + substreams protogen --exclude-paths sf/substreams,google + +.PHONY: tt +tt: + substreams run -e $(ENDPOINT) substreams.yaml graph_out -s 18384526 -t +10 + +.PHONY: pack +pack: + substreams pack + +.PHONY: graph +graph: + substreams graph + +.PHONY: info +info: + substreams info + +.PHONY: run +run: + substreams run map_block -e eth.substreams.pinax.network:9000 -s -1000 -o jsonl + +.PHONY: gui +gui: + substreams gui map_block -e eth.substreams.pinax.network:9000 -s 447766 + +.PHONY: deploy +deploy: + graph deploy --studio erc-20 diff --git a/erc20-balances/README.md b/erc20-balances/README.md new file mode 100644 index 0000000..515946e --- /dev/null +++ b/erc20-balances/README.md @@ -0,0 +1,159 @@ +# Substreams ERC20 Balance Changes +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +The goal of this Substreams project is to extract all ERC20 transfers from Ethereum events for the full chain. + +The `map_balance_changes` module will output messages of type `erc20.types.v1.BalanceChange` defined by: + +```proto +message BalanceChange { +string contract = 1; +string owner = 2; +string new_balance = 4; +BalanceChangeType change_type = 9; +} +``` + +## Known issues: + +Tracking balance changes requires tracking state changes on chain. However, different contracts have different ways of storing balances. + +We have implemented the following strategies for tracking balance changes: + +### Type 1: Storage change is in the same call as the transfer + +example: +https://etherscan.io/tx/0xf490320cff087d82747fcb0e6ed797f899ff887bcd15162933ea051c94c596ea#eventlog + +Here is the relevant section from the Firehose block for this transaction: + +```json + { + "index": 1, + "callType": "CALL", + "caller": "45225d3536ac02928f16071ab05066bce95c2cd5", + "address": "dac17f958d2ee523a2206206994597c13d831ec7", + "gasLimit": "104810", + "gasConsumed": "41601", + "input": "a9059cbb000000000000000000000000caf7ce56598e8588c9bf471e08b53e8a8d9541b300000000000000000000000000000000000000000000000000000000c84cfb23", + "executedCode": true, + "keccakPreimages": { + "3cacfdf5e3a27369ea8efd976a1d467ed2ce08586e22e7366aa4d82943439fa7": "00000000000000000000000045225d3536ac02928f16071ab05066bce95c2cd50000000000000000000000000000000000000000000000000000000000000006", + "d116b96c704431079cf20227b36d5f02fea21af673489300fe1ae3229e0c0d74": "000000000000000000000000caf7ce56598e8588c9bf471e08b53e8a8d9541b30000000000000000000000000000000000000000000000000000000000000002", + "ec2750738b8e716c607ab9d95b2d48bc4d6b8eacc278d1510c490ab2c788884d": "00000000000000000000000045225d3536ac02928f16071ab05066bce95c2cd50000000000000000000000000000000000000000000000000000000000000002" + }, + "storageChanges": [ + { + "address": "dac17f958d2ee523a2206206994597c13d831ec7", + "key": "ec2750738b8e716c607ab9d95b2d48bc4d6b8eacc278d1510c490ab2c788884d", + "oldValue": "000000000000000000000000000000000000000000000000000000355ed4c80e", + "newValue": "000000000000000000000000000000000000000000000000000000349687cceb", + "ordinal": "1154" + }, + { + "address": "dac17f958d2ee523a2206206994597c13d831ec7", + "key": "d116b96c704431079cf20227b36d5f02fea21af673489300fe1ae3229e0c0d74", + "oldValue": "0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "00000000000000000000000000000000000000000000000000000000c84cfb23", + "ordinal": "1155" + } + ], + "logs": [ + { + "address": "dac17f958d2ee523a2206206994597c13d831ec7", + "topics": [ + "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "00000000000000000000000045225d3536ac02928f16071ab05066bce95c2cd5", + "000000000000000000000000caf7ce56598e8588c9bf471e08b53e8a8d9541b3" + ], + "data": "00000000000000000000000000000000000000000000000000000000c84cfb23", + "blockIndex": 49, + "ordinal": "1157" + } + ] + } +``` + +The correctness of the `old_balance` and `new_balance` values in this case is easily determined. + +These types of transfers will result in a BalanceChange message with `change_type` set to `TYPE_1`. + +### Type 2: Storage change is in a different call than the transfer + +In this case, the Transfer but this results in storage changes in different child calls, where often the amount sent will be split to multiple accounts. + +example: +https://etherscan.io/tx/0x5a31fb5d3f5bbb95023438f017ad6cd501ce70e445f31c2660c784e5a7eb5d83#eventlog + +```json +{ + "index": 4, + "logs": [ + { + "address": "225bc3affc1da39bd3cb2100c74a41c62310d1e1", + "topics": [ + "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "000000000000000000000000541f52216afdfeef6851eea9772b17d3cafd9438", + "000000000000000000000000b30acc73814d34941d71a1dfa5c2a5e618a062fe" + ], + "data": "0000000000000000000000000000000000000000000000000000000000451f50", + "index": 2, + "blockIndex": 2, + "ordinal": "68" + } + ] +}, +{ + "index": 10, + "keccakPreimages": { + "c0309ad5a3dcaf0d46cab6102b742e914f7ff8447190f509bf80a0f0b60c452c": "000000000000000000000000b30acc73814d34941d71a1dfa5c2a5e618a062fe0000000000000000000000000000000000000000000000000000000000000002" + }, + "storageChanges": [ + { + "address": "276c5c6ca8507ed7bac085fc9b9521f4f54b58d3", + "key": "c0309ad5a3dcaf0d46cab6102b742e914f7ff8447190f509bf80a0f0b60c452c", + "oldValue": "000000000000000000000000000000000000000000000000000000012d03e73e", + "newValue": "000000000000000000000000000000000000000000000000000000012d48915e", + "ordinal": "61" + } + ], +} +``` + +In this example, the Transfer call is made in call index 4. Then in the subsequent child calls, the transfer of 4,530,000 tokens is split into transfers by the contract: One transfer of 4,500,000 to the original receiver and a transfer of 30,000 to another address. Some work is required to track the balance changes in this case. + +These types of transfers will result in a BalanceChange message with `change_type` set to `TYPE_2`. + +### Others + +There are other types of transfers where the balance of the accounts before and after is not clear. + +example: +https://etherscan.io/tx/0x5a31fb5d3f5bbb95023438f017ad6cd501ce70e445f31c2660c784e5a7eb5d83#eventlog + +These transfers will result in a BalanceChange message with `change_type` set to `null`. + +These should currently be discarded by the consumer of the substream as they are guaranteed to be incorrect. + + +## Running + +### Generate protos + +```bash +make protogen +``` + +### Build substreams + +```bash +make build +``` + +### Build spkg + +```bash +make pack +``` + + diff --git a/erc20-balances/abi/erc20.json b/erc20-balances/abi/erc20.json new file mode 100644 index 0000000..3b0ab2f --- /dev/null +++ b/erc20-balances/abi/erc20.json @@ -0,0 +1,222 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "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": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "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": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "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": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "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": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] \ No newline at end of file diff --git a/erc20-balances/build.rs b/erc20-balances/build.rs new file mode 100644 index 0000000..59b3228 --- /dev/null +++ b/erc20-balances/build.rs @@ -0,0 +1,9 @@ +use anyhow::{Ok, Result}; +use substreams_ethereum::Abigen; + +fn main() -> Result<(), anyhow::Error> { + Abigen::new("erc20", "abi/erc20.json")? + .generate()? + .write_to_file("src/abi/erc20.rs")?; + Ok(()) +} \ No newline at end of file diff --git a/erc20-balances/package.json b/erc20-balances/package.json new file mode 100644 index 0000000..4a6feff --- /dev/null +++ b/erc20-balances/package.json @@ -0,0 +1,13 @@ +{ + "name": "erc20-balances", + "license": "UNLICENSED", + "scripts": { + "build": "graph build", + "deploy": "graph deploy --product hosted-service gundamdweeb/erc20-balance-with-token", + "create-local": "graph create --node http://localhost:8020/ gundamdweeb/erc20-balance-with-token", + "remove-local": "graph remove --node http://localhost:8020/ gundamdweeb/erc20-balance-with-token", + "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 gundamdweeb/erc20-balance-with-token", + "test": "graph test" + }, + "dependencies": { "@graphprotocol/graph-cli": "0.51.1" } +} diff --git a/erc20-balances/proto/v1/erc20.proto b/erc20-balances/proto/v1/erc20.proto new file mode 100644 index 0000000..b563896 --- /dev/null +++ b/erc20-balances/proto/v1/erc20.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package erc20.types.v1; + +message BalanceChanges { + repeated BalanceChange balance_changes = 1; +} + +enum BalanceChangeType { + TYPE_UNKNOWN = 0; // cannot determine balance change + TYPE_1 = 1; // easy case where storage change is in the same call as the Transfer call + TYPE_2 = 2; // storage change is in a different call than the Transfer call +} + +message BalanceChange { + string contract = 1; + string owner = 2; + string new_balance = 4; + BalanceChangeType change_type = 9; +} + +message Erc20Token { + string address = 1; + string name = 2; + string symbol = 3; + uint64 decimals = 4; +} + + diff --git a/erc20-balances/rust-toolchain.toml b/erc20-balances/rust-toolchain.toml new file mode 100644 index 0000000..ebb038a --- /dev/null +++ b/erc20-balances/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.69.0" +components = [ "rustfmt" ] +targets = [ "wasm32-unknown-unknown" ] \ No newline at end of file diff --git a/erc20-balances/schema.graphql b/erc20-balances/schema.graphql new file mode 100644 index 0000000..b7aabdc --- /dev/null +++ b/erc20-balances/schema.graphql @@ -0,0 +1,18 @@ +type Account @entity { + id: ID! + balances: [Balance!]! @derivedFrom(field: "owner") +} + +type Token @entity { + id: ID! + name: String! + decimals: BigInt! + symbol: String! +} + +type Balance @entity { + id: ID! + token: Token! + owner: Account! + balance: BigInt! +} \ No newline at end of file diff --git a/erc20-balances/src/abi/erc20.rs b/erc20-balances/src/abi/erc20.rs new file mode 100644 index 0000000..b7b3a39 --- /dev/null +++ b/erc20-balances/src/abi/erc20.rs @@ -0,0 +1,1257 @@ + const INTERNAL_ERR: &'static str = "`ethabi_derive` internal error"; + /// Contract's functions. + #[allow(dead_code, unused_imports, unused_variables)] + pub mod functions { + use super::INTERNAL_ERR; + #[derive(Debug, Clone, PartialEq)] + pub struct Allowance { + pub owner: Vec, + pub spender: Vec, + } + impl Allowance { + const METHOD_ID: [u8; 4] = [221u8, 98u8, 237u8, 62u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + let maybe_data = call.input.get(4..); + if maybe_data.is_none() { + return Err("no data to decode".to_string()); + } + let mut values = ethabi::decode( + &[ethabi::ParamType::Address, ethabi::ParamType::Address], + maybe_data.unwrap(), + ) + .map_err(|e| format!("unable to decode call.input: {:?}", e))?; + values.reverse(); + Ok(Self { + owner: values + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + spender: values + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + }) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode( + &[ + ethabi::Token::Address(ethabi::Address::from_slice(&self.owner)), + ethabi::Token::Address( + ethabi::Address::from_slice(&self.spender), + ), + ], + ); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Uint(256usize)], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok({ + let mut v = [0 as u8; 32]; + values + .pop() + .expect("one output data should have existed") + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for Allowance { + const NAME: &'static str = "allowance"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable + for Allowance { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct Approve { + pub spender: Vec, + pub value: substreams::scalar::BigInt, + } + impl Approve { + const METHOD_ID: [u8; 4] = [9u8, 94u8, 167u8, 179u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + let maybe_data = call.input.get(4..); + if maybe_data.is_none() { + return Err("no data to decode".to_string()); + } + let mut values = ethabi::decode( + &[ethabi::ParamType::Address, ethabi::ParamType::Uint(256usize)], + maybe_data.unwrap(), + ) + .map_err(|e| format!("unable to decode call.input: {:?}", e))?; + values.reverse(); + Ok(Self { + spender: values + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + value: { + let mut v = [0 as u8; 32]; + values + .pop() + .expect(INTERNAL_ERR) + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }, + }) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode( + &[ + ethabi::Token::Address( + ethabi::Address::from_slice(&self.spender), + ), + ethabi::Token::Uint( + ethabi::Uint::from_big_endian( + match self.value.clone().to_bytes_be() { + (num_bigint::Sign::Plus, bytes) => bytes, + (num_bigint::Sign::NoSign, bytes) => bytes, + (num_bigint::Sign::Minus, _) => { + panic!("negative numbers are not supported") + } + } + .as_slice(), + ), + ), + ], + ); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Bool], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok( + values + .pop() + .expect("one output data should have existed") + .into_bool() + .expect(INTERNAL_ERR), + ) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for Approve { + const NAME: &'static str = "approve"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable for Approve { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct BalanceOf { + pub owner: Vec, + } + impl BalanceOf { + const METHOD_ID: [u8; 4] = [112u8, 160u8, 130u8, 49u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + let maybe_data = call.input.get(4..); + if maybe_data.is_none() { + return Err("no data to decode".to_string()); + } + let mut values = ethabi::decode( + &[ethabi::ParamType::Address], + maybe_data.unwrap(), + ) + .map_err(|e| format!("unable to decode call.input: {:?}", e))?; + values.reverse(); + Ok(Self { + owner: values + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + }) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode( + &[ethabi::Token::Address(ethabi::Address::from_slice(&self.owner))], + ); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Uint(256usize)], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok({ + let mut v = [0 as u8; 32]; + values + .pop() + .expect("one output data should have existed") + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for BalanceOf { + const NAME: &'static str = "balanceOf"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable + for BalanceOf { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct Decimals {} + impl Decimals { + const METHOD_ID: [u8; 4] = [49u8, 60u8, 229u8, 103u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Ok(Self {}) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode(&[]); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Uint(8usize)], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok({ + let mut v = [0 as u8; 32]; + values + .pop() + .expect("one output data should have existed") + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for Decimals { + const NAME: &'static str = "decimals"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable + for Decimals { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct Name {} + impl Name { + const METHOD_ID: [u8; 4] = [6u8, 253u8, 222u8, 3u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Ok(Self {}) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode(&[]); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::String], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok( + values + .pop() + .expect("one output data should have existed") + .into_string() + .expect(INTERNAL_ERR), + ) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for Name { + const NAME: &'static str = "name"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable for Name { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct Symbol {} + impl Symbol { + const METHOD_ID: [u8; 4] = [149u8, 216u8, 155u8, 65u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Ok(Self {}) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode(&[]); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::String], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok( + values + .pop() + .expect("one output data should have existed") + .into_string() + .expect(INTERNAL_ERR), + ) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for Symbol { + const NAME: &'static str = "symbol"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable for Symbol { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct TotalSupply {} + impl TotalSupply { + const METHOD_ID: [u8; 4] = [24u8, 22u8, 13u8, 221u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Ok(Self {}) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode(&[]); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Uint(256usize)], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok({ + let mut v = [0 as u8; 32]; + values + .pop() + .expect("one output data should have existed") + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for TotalSupply { + const NAME: &'static str = "totalSupply"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable + for TotalSupply { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct Transfer { + pub to: Vec, + pub value: substreams::scalar::BigInt, + } + impl Transfer { + const METHOD_ID: [u8; 4] = [169u8, 5u8, 156u8, 187u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + let maybe_data = call.input.get(4..); + if maybe_data.is_none() { + return Err("no data to decode".to_string()); + } + let mut values = ethabi::decode( + &[ethabi::ParamType::Address, ethabi::ParamType::Uint(256usize)], + maybe_data.unwrap(), + ) + .map_err(|e| format!("unable to decode call.input: {:?}", e))?; + values.reverse(); + Ok(Self { + to: values + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + value: { + let mut v = [0 as u8; 32]; + values + .pop() + .expect(INTERNAL_ERR) + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }, + }) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode( + &[ + ethabi::Token::Address(ethabi::Address::from_slice(&self.to)), + ethabi::Token::Uint( + ethabi::Uint::from_big_endian( + match self.value.clone().to_bytes_be() { + (num_bigint::Sign::Plus, bytes) => bytes, + (num_bigint::Sign::NoSign, bytes) => bytes, + (num_bigint::Sign::Minus, _) => { + panic!("negative numbers are not supported") + } + } + .as_slice(), + ), + ), + ], + ); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Bool], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok( + values + .pop() + .expect("one output data should have existed") + .into_bool() + .expect(INTERNAL_ERR), + ) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for Transfer { + const NAME: &'static str = "transfer"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable for Transfer { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct TransferFrom { + pub from: Vec, + pub to: Vec, + pub value: substreams::scalar::BigInt, + } + impl TransferFrom { + const METHOD_ID: [u8; 4] = [35u8, 184u8, 114u8, 221u8]; + pub fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + let maybe_data = call.input.get(4..); + if maybe_data.is_none() { + return Err("no data to decode".to_string()); + } + let mut values = ethabi::decode( + &[ + ethabi::ParamType::Address, + ethabi::ParamType::Address, + ethabi::ParamType::Uint(256usize), + ], + maybe_data.unwrap(), + ) + .map_err(|e| format!("unable to decode call.input: {:?}", e))?; + values.reverse(); + Ok(Self { + from: values + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + to: values + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + value: { + let mut v = [0 as u8; 32]; + values + .pop() + .expect(INTERNAL_ERR) + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }, + }) + } + pub fn encode(&self) -> Vec { + let data = ethabi::encode( + &[ + ethabi::Token::Address(ethabi::Address::from_slice(&self.from)), + ethabi::Token::Address(ethabi::Address::from_slice(&self.to)), + ethabi::Token::Uint( + ethabi::Uint::from_big_endian( + match self.value.clone().to_bytes_be() { + (num_bigint::Sign::Plus, bytes) => bytes, + (num_bigint::Sign::NoSign, bytes) => bytes, + (num_bigint::Sign::Minus, _) => { + panic!("negative numbers are not supported") + } + } + .as_slice(), + ), + ), + ], + ); + let mut encoded = Vec::with_capacity(4 + data.len()); + encoded.extend(Self::METHOD_ID); + encoded.extend(data); + encoded + } + pub fn output_call( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::output(call.return_data.as_ref()) + } + pub fn output(data: &[u8]) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Bool], + data.as_ref(), + ) + .map_err(|e| format!("unable to decode output data: {:?}", e))?; + Ok( + values + .pop() + .expect("one output data should have existed") + .into_bool() + .expect(INTERNAL_ERR), + ) + } + pub fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + match call.input.get(0..4) { + Some(signature) => Self::METHOD_ID == signature, + None => false, + } + } + pub fn call(&self, address: Vec) -> Option { + use substreams_ethereum::pb::eth::rpc; + let rpc_calls = rpc::RpcCalls { + calls: vec![ + rpc::RpcCall { to_addr : address, data : self.encode(), } + ], + }; + let responses = substreams_ethereum::rpc::eth_call(&rpc_calls).responses; + let response = responses + .get(0) + .expect("one response should have existed"); + if response.failed { + return None; + } + match Self::output(response.raw.as_ref()) { + Ok(data) => Some(data), + Err(err) => { + use substreams_ethereum::Function; + substreams::log::info!( + "Call output for function `{}` failed to decode with error: {}", + Self::NAME, err + ); + None + } + } + } + } + impl substreams_ethereum::Function for TransferFrom { + const NAME: &'static str = "transferFrom"; + fn match_call(call: &substreams_ethereum::pb::eth::v2::Call) -> bool { + Self::match_call(call) + } + fn decode( + call: &substreams_ethereum::pb::eth::v2::Call, + ) -> Result { + Self::decode(call) + } + fn encode(&self) -> Vec { + self.encode() + } + } + impl substreams_ethereum::rpc::RPCDecodable for TransferFrom { + fn output(data: &[u8]) -> Result { + Self::output(data) + } + } + } + /// Contract's events. + #[allow(dead_code, unused_imports, unused_variables)] + pub mod events { + use super::INTERNAL_ERR; + #[derive(Debug, Clone, PartialEq)] + pub struct Approval { + pub owner: Vec, + pub spender: Vec, + pub value: substreams::scalar::BigInt, + } + impl Approval { + const TOPIC_ID: [u8; 32] = [ + 140u8, + 91u8, + 225u8, + 229u8, + 235u8, + 236u8, + 125u8, + 91u8, + 209u8, + 79u8, + 113u8, + 66u8, + 125u8, + 30u8, + 132u8, + 243u8, + 221u8, + 3u8, + 20u8, + 192u8, + 247u8, + 178u8, + 41u8, + 30u8, + 91u8, + 32u8, + 10u8, + 200u8, + 199u8, + 195u8, + 185u8, + 37u8, + ]; + pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool { + if log.topics.len() != 3usize { + return false; + } + if log.data.len() != 32usize { + return false; + } + return log.topics.get(0).expect("bounds already checked").as_ref() + == Self::TOPIC_ID; + } + pub fn decode( + log: &substreams_ethereum::pb::eth::v2::Log, + ) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Uint(256usize)], + log.data.as_ref(), + ) + .map_err(|e| format!("unable to decode log.data: {:?}", e))?; + values.reverse(); + Ok(Self { + owner: ethabi::decode( + &[ethabi::ParamType::Address], + log.topics[1usize].as_ref(), + ) + .map_err(|e| { + format!( + "unable to decode param 'owner' from topic of type 'address': {:?}", + e + ) + })? + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + spender: ethabi::decode( + &[ethabi::ParamType::Address], + log.topics[2usize].as_ref(), + ) + .map_err(|e| { + format!( + "unable to decode param 'spender' from topic of type 'address': {:?}", + e + ) + })? + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + value: { + let mut v = [0 as u8; 32]; + values + .pop() + .expect(INTERNAL_ERR) + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }, + }) + } + } + impl substreams_ethereum::Event for Approval { + const NAME: &'static str = "Approval"; + fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool { + Self::match_log(log) + } + fn decode( + log: &substreams_ethereum::pb::eth::v2::Log, + ) -> Result { + Self::decode(log) + } + } + #[derive(Debug, Clone, PartialEq)] + pub struct Transfer { + pub from: Vec, + pub to: Vec, + pub value: substreams::scalar::BigInt, + } + impl Transfer { + const TOPIC_ID: [u8; 32] = [ + 221u8, + 242u8, + 82u8, + 173u8, + 27u8, + 226u8, + 200u8, + 155u8, + 105u8, + 194u8, + 176u8, + 104u8, + 252u8, + 55u8, + 141u8, + 170u8, + 149u8, + 43u8, + 167u8, + 241u8, + 99u8, + 196u8, + 161u8, + 22u8, + 40u8, + 245u8, + 90u8, + 77u8, + 245u8, + 35u8, + 179u8, + 239u8, + ]; + pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool { + if log.topics.len() != 3usize { + return false; + } + if log.data.len() != 32usize { + return false; + } + return log.topics.get(0).expect("bounds already checked").as_ref() + == Self::TOPIC_ID; + } + pub fn decode( + log: &substreams_ethereum::pb::eth::v2::Log, + ) -> Result { + let mut values = ethabi::decode( + &[ethabi::ParamType::Uint(256usize)], + log.data.as_ref(), + ) + .map_err(|e| format!("unable to decode log.data: {:?}", e))?; + values.reverse(); + Ok(Self { + from: ethabi::decode( + &[ethabi::ParamType::Address], + log.topics[1usize].as_ref(), + ) + .map_err(|e| { + format!( + "unable to decode param 'from' from topic of type 'address': {:?}", + e + ) + })? + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + to: ethabi::decode( + &[ethabi::ParamType::Address], + log.topics[2usize].as_ref(), + ) + .map_err(|e| { + format!( + "unable to decode param 'to' from topic of type 'address': {:?}", + e + ) + })? + .pop() + .expect(INTERNAL_ERR) + .into_address() + .expect(INTERNAL_ERR) + .as_bytes() + .to_vec(), + value: { + let mut v = [0 as u8; 32]; + values + .pop() + .expect(INTERNAL_ERR) + .into_uint() + .expect(INTERNAL_ERR) + .to_big_endian(v.as_mut_slice()); + substreams::scalar::BigInt::from_unsigned_bytes_be(&v) + }, + }) + } + } + impl substreams_ethereum::Event for Transfer { + const NAME: &'static str = "Transfer"; + fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool { + Self::match_log(log) + } + fn decode( + log: &substreams_ethereum::pb::eth::v2::Log, + ) -> Result { + Self::decode(log) + } + } + } \ No newline at end of file diff --git a/erc20-balances/src/abi/mod.rs b/erc20-balances/src/abi/mod.rs new file mode 100644 index 0000000..1433e08 --- /dev/null +++ b/erc20-balances/src/abi/mod.rs @@ -0,0 +1,2 @@ +#[allow(unused_imports)] +pub mod erc20; diff --git a/erc20-balances/src/lib.rs b/erc20-balances/src/lib.rs new file mode 100644 index 0000000..e9ca408 --- /dev/null +++ b/erc20-balances/src/lib.rs @@ -0,0 +1,5 @@ +mod maps; +mod sinks; +mod abi; +mod pb; +mod utils; \ No newline at end of file diff --git a/erc20-balances/src/maps.rs b/erc20-balances/src/maps.rs new file mode 100644 index 0000000..45d62c9 --- /dev/null +++ b/erc20-balances/src/maps.rs @@ -0,0 +1,314 @@ + + +use std::collections::HashMap; + +use crate::abi::erc20::events::Transfer; +use crate::utils::helper::{get_erc20_token}; + +use crate::pb::erc20::types::v1::{ + Erc20Token, BalanceChanges, BalanceChange, BalanceChangeType +}; +use substreams::errors::Error; +use substreams::log::info; +use substreams::scalar::BigInt; +use substreams::{Hex, hex}; +use substreams::store::{StoreNew, StoreSetIfNotExistsProto, StoreSetIfNotExists, StoreSetIfNotExistsString}; +use substreams_ethereum::Event; +use substreams_ethereum::pb::eth::v2::{Block, Call, TransactionTrace, TransactionTraceStatus}; + +const NULL_ADDRESS: [u8; 20] = hex!("0000000000000000000000000000000000000000"); +const ZERO_STORAGE_PREFIX: [u8; 16] = hex!("00000000000000000000000000000000"); + +#[substreams::handlers::map] +pub fn map_block(block: Block) -> Result { + let balance_changes = map_balance_change(block); + + Ok(BalanceChanges { + balance_changes + }) +} + +#[substreams::handlers::store] +pub fn store_tokens(i0: BalanceChanges, o: StoreSetIfNotExistsString) { + for balance_change in i0.balance_changes { + o.set_if_not_exists( + 0, + &balance_change.contract, + &balance_change.contract + ); + } +} + + + +pub fn map_balance_change(block: Block) -> Vec { + let mut balance_changes = Vec::new(); + + for trx in block.transaction_traces.iter() { + if trx.status != TransactionTraceStatus::Succeeded as i32 { + continue; + } + + for call in trx.calls.iter() { + if call.state_reverted { + continue; + } + + for log in call.logs.iter() { + let transfer = match Transfer::match_and_decode(log) { + Some(transfer) => transfer, + None => continue, + }; + + if transfer.value.is_zero() { + continue; + } + + if transfer.from == NULL_ADDRESS { + continue; + } + + // Trying with algorithm #1 + let mut found_balance_changes = + find_erc20_balance_changes_algorithm1(trx, call, &transfer); + if !found_balance_changes.is_empty() { + balance_changes.extend(found_balance_changes); + continue; + } + + // No balance changes found using algorithm #1, trying with algorithm #2 + found_balance_changes = find_erc20_balance_changes_algorithm2(&transfer, &call, trx); + if !found_balance_changes.is_empty() { + balance_changes.extend(found_balance_changes); + continue; + } + + // No algorithm could extract the balance change, old/new balance is fixed at 0 + balance_changes.push(BalanceChange { + contract: Hex::encode(&call.address), + owner: Hex::encode(&transfer.to), + new_balance: "0".to_string(), + change_type: BalanceChangeType::TypeUnknown as i32, + }); + + } + } + } + + balance_changes +} + +/// normal case +fn find_erc20_balance_changes_algorithm1( + trx: &TransactionTrace, + call: &Call, + transfer: &Transfer, +) -> Vec { + let mut out = Vec::new(); + let mut keccak_address_map: Option = None; + + for storage_change in &call.storage_changes { + let old_balance = BigInt::from_signed_bytes_be(&storage_change.old_value); + let new_balance = BigInt::from_signed_bytes_be(&storage_change.new_value); + + let balance_change = new_balance - old_balance; + let balance_change_abs = if balance_change < BigInt::zero() { + balance_change.neg() + } else { + balance_change + }; + + let value = transfer.value.clone(); + let transfer_value_abs = if value.clone() < BigInt::zero() { + value.neg() + } else { + value.clone() + }; + + if balance_change_abs != transfer_value_abs { + info!("Balance change does not match transfer value. Balance change: {}, transfer value: {}", balance_change_abs, transfer_value_abs); + continue; + } + + // We memoize the keccak address map by call because it is expensive to compute + if keccak_address_map.is_none() { + keccak_address_map = Some(erc20_addresses_for_storage_keys(call)); + } + + let keccak_address = match keccak_address_map + .as_ref() + .unwrap() + .get(&storage_change.key) + { + Some(address) => address, + None => { + if storage_change.key[0..16] == ZERO_STORAGE_PREFIX { + info!("Skipping balance change for zero key"); + continue; + } + + info!( + "No keccak address found for key: {}, trx {}", + Hex(&storage_change.key), + Hex(&trx.hash) + ); + continue; + } + }; + + if !erc20_is_valid_address(keccak_address, transfer) { + info!("Keccak address does not match transfer address. Keccak address: {}, sender address: {}, receiver address: {}, trx {}", Hex(keccak_address), Hex(&transfer.from), Hex(&transfer.to), Hex(&trx.hash)); + continue; + } + + let change = BalanceChange { + contract: Hex::encode(&storage_change.address), + owner: Hex::encode(keccak_address), + new_balance: BigInt::from_signed_bytes_be(&storage_change.new_value).to_string(), + change_type: BalanceChangeType::Type1 as i32, + }; + + out.push(change); + } + + out +} + +// case where storage changes are not in the same call as the transfer event +fn find_erc20_balance_changes_algorithm2( + transfer: &Transfer, + original_call: &Call, + trx: &TransactionTrace, +) -> Vec { + let mut out = Vec::new(); + + //get all keccak keys for transfer.to and transfer.from + + let mut keys = HashMap::new(); + for call in trx.calls.iter() { + let keccak_address_map = erc20_addresses_for_storage_keys(call); + keys.extend(keccak_address_map); + } + + let child_calls = get_all_child_calls(original_call, trx); + + //get all storage changes for these calls: + let mut storage_changes = Vec::new(); + for call in child_calls.iter() { + storage_changes.extend(call.storage_changes.clone()); + } + + let mut total_sent = BigInt::zero(); + let mut total_received = BigInt::zero(); + + //check if any of the storage changes match the transfer.to or transfer.from + for storage_change in storage_changes.clone().iter() { + let keccak_address = match keys.get(&storage_change.key) { + Some(address) => address, + None => continue, + }; + + if !erc20_is_valid_address(keccak_address, transfer) { + continue; + } + + let old_balance = BigInt::from_signed_bytes_be(&storage_change.old_value); + let new_balance = BigInt::from_signed_bytes_be(&storage_change.new_value); + + let balance_change = new_balance - old_balance; + if balance_change < BigInt::zero() { + total_sent = total_sent + balance_change.neg(); + } else { + total_received = total_received + balance_change; + }; + + let change = BalanceChange { + contract: Hex::encode(&storage_change.address), + owner: Hex::encode(keccak_address), + new_balance: BigInt::from_signed_bytes_be(&storage_change.new_value).to_string(), + change_type: BalanceChangeType::Type2 as i32, + }; + + out.push(change); + } + + if total_sent == transfer.value { + return out; + } + + let mut diff = total_sent - total_received; + if diff < BigInt::zero() { + diff = diff.neg(); + } + + //look for a storage change that matches the diff + for storage_change in storage_changes.iter() { + let keccak_address = match keys.get(&storage_change.key) { + Some(address) => address, + None => continue, + }; + + let old_balance = BigInt::from_signed_bytes_be(&storage_change.old_value); + let new_balance = BigInt::from_signed_bytes_be(&storage_change.new_value); + + let mut balance_change = new_balance - old_balance; + if balance_change < BigInt::zero() { + balance_change = balance_change.neg(); + } + + if balance_change != diff { + continue; + } + + let change = BalanceChange { + contract: Hex::encode(&storage_change.address), + owner: Hex::encode(keccak_address), + new_balance: BigInt::from_signed_bytes_be(&storage_change.new_value).to_string(), + change_type: BalanceChangeType::Type2 as i32, + }; + + out.push(change); + } + + out +} + +type StorageKeyToAddressMap = HashMap, Vec>; + +fn erc20_addresses_for_storage_keys(call: &Call) -> StorageKeyToAddressMap { + let mut out = HashMap::new(); + + for (hash, preimage) in &call.keccak_preimages { + if preimage.len() != 128 { + continue; + } + + if &preimage[64..126] != "00000000000000000000000000000000000000000000000000000000000000" { + continue; + } + + let addr = &preimage[24..64]; + out.insert( + Hex::decode(hash).expect("Failed to decode hash hex string"), + Hex::decode(addr).expect("Failed to decode address hex string"), + ); + } + + out +} + +fn erc20_is_valid_address(address: &Vec, transfer: &Transfer) -> bool { + address == &transfer.from || address == &transfer.to +} + +fn get_all_child_calls(original: &Call, trx: &TransactionTrace) -> Vec { + let mut out = Vec::new(); + + for call in trx.calls.iter() { + if call.parent_index == original.index { + out.push(call.clone()); + } + } + + out +} \ No newline at end of file diff --git a/erc20-balances/src/pb/erc20.types.v1.rs b/erc20-balances/src/pb/erc20.types.v1.rs new file mode 100644 index 0000000..e398c06 --- /dev/null +++ b/erc20-balances/src/pb/erc20.types.v1.rs @@ -0,0 +1,64 @@ +// @generated +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BalanceChanges { + #[prost(message, repeated, tag="1")] + pub balance_changes: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BalanceChange { + #[prost(string, tag="1")] + pub contract: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub owner: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub new_balance: ::prost::alloc::string::String, + #[prost(enumeration="BalanceChangeType", tag="9")] + pub change_type: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Erc20Token { + #[prost(string, tag="1")] + pub address: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub name: ::prost::alloc::string::String, + #[prost(string, tag="3")] + pub symbol: ::prost::alloc::string::String, + #[prost(uint64, tag="4")] + pub decimals: u64, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum BalanceChangeType { + /// cannot determine balance change + TypeUnknown = 0, + /// easy case where storage change is in the same call as the Transfer call + Type1 = 1, + /// storage change is in a different call than the Transfer call + Type2 = 2, +} +impl BalanceChangeType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + BalanceChangeType::TypeUnknown => "TYPE_UNKNOWN", + BalanceChangeType::Type1 => "TYPE_1", + BalanceChangeType::Type2 => "TYPE_2", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "TYPE_UNKNOWN" => Some(Self::TypeUnknown), + "TYPE_1" => Some(Self::Type1), + "TYPE_2" => Some(Self::Type2), + _ => None, + } + } +} +// @@protoc_insertion_point(module) diff --git a/erc20-balances/src/pb/mod.rs b/erc20-balances/src/pb/mod.rs new file mode 100644 index 0000000..9a6928d --- /dev/null +++ b/erc20-balances/src/pb/mod.rs @@ -0,0 +1,10 @@ +// @generated +pub mod erc20 { + pub mod types { + // @@protoc_insertion_point(attribute:erc20.types.v1) + pub mod v1 { + include!("erc20.types.v1.rs"); + // @@protoc_insertion_point(erc20.types.v1) + } + } +} diff --git a/erc20-balances/src/pb/pinax.substreams.sink.prometheus.v1.rs b/erc20-balances/src/pb/pinax.substreams.sink.prometheus.v1.rs new file mode 100644 index 0000000..84f7119 --- /dev/null +++ b/erc20-balances/src/pb/pinax.substreams.sink.prometheus.v1.rs @@ -0,0 +1,276 @@ +// @generated +/// Vector of Prometheus metrics +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PrometheusOperations { + #[prost(message, repeated, tag="1")] + pub operations: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PrometheusOperation { + /// Name of the Prometheus metric + #[prost(string, tag="1")] + pub name: ::prost::alloc::string::String, + /// Labels represents a collection of label name -> value mappings. + #[prost(map="string, string", tag="2")] + pub labels: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, + #[prost(oneof="prometheus_operation::Operation", tags="3, 4, 5, 6")] + pub operation: ::core::option::Option, +} +/// Nested message and enum types in `PrometheusOperation`. +pub mod prometheus_operation { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Operation { + #[prost(message, tag="3")] + Gauge(super::GaugeOp), + #[prost(message, tag="4")] + Counter(super::CounterOp), + #[prost(message, tag="5")] + Histogram(super::HistogramOp), + #[prost(message, tag="6")] + Summary(super::SummaryOp), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GaugeOp { + #[prost(enumeration="gauge_op::Operation", tag="1")] + pub operation: i32, + /// Value (Float) to be used in the operation + #[prost(double, tag="2")] + pub value: f64, +} +/// Nested message and enum types in `GaugeOp`. +pub mod gauge_op { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Operation { + /// Protobuf default should not be used, this is used so that the consume can ensure that the value was actually specified + Unspecified = 0, + /// Inc increments the Gauge by 1. Use Add to increment it by arbitrary values. + Inc = 1, + /// Add adds the given value to the Gauge. (The value can be negative, resulting in a decrease of the Gauge.) + /// + /// float + Add = 2, + /// Set sets the Gauge to an arbitrary value. + /// + /// float + Set = 3, + /// Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary values. + Dec = 4, + /// Sub subtracts the given value from the Gauge. (The value can be negative, resulting in an increase of the Gauge.) + /// + /// float + Sub = 5, + /// SetToCurrentTime sets the Gauge to the current Unix time in seconds. + SetToCurrentTime = 6, + /// Remove metrics for the given label values + Remove = 7, + /// Reset gauge values + Reset = 8, + } + impl Operation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Operation::Unspecified => "OPERATION_UNSPECIFIED", + Operation::Inc => "OPERATION_INC", + Operation::Add => "OPERATION_ADD", + Operation::Set => "OPERATION_SET", + Operation::Dec => "OPERATION_DEC", + Operation::Sub => "OPERATION_SUB", + Operation::SetToCurrentTime => "OPERATION_SET_TO_CURRENT_TIME", + Operation::Remove => "OPERATION_REMOVE", + Operation::Reset => "OPERATION_RESET", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "OPERATION_UNSPECIFIED" => Some(Self::Unspecified), + "OPERATION_INC" => Some(Self::Inc), + "OPERATION_ADD" => Some(Self::Add), + "OPERATION_SET" => Some(Self::Set), + "OPERATION_DEC" => Some(Self::Dec), + "OPERATION_SUB" => Some(Self::Sub), + "OPERATION_SET_TO_CURRENT_TIME" => Some(Self::SetToCurrentTime), + "OPERATION_REMOVE" => Some(Self::Remove), + "OPERATION_RESET" => Some(Self::Reset), + _ => None, + } + } + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CounterOp { + #[prost(enumeration="counter_op::Operation", tag="1")] + pub operation: i32, + /// Value (Float) to be used in the operation + #[prost(double, tag="2")] + pub value: f64, +} +/// Nested message and enum types in `CounterOp`. +pub mod counter_op { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Operation { + /// Protobuf default should not be used, this is used so that the consume can ensure that the value was actually specified + Unspecified = 0, + /// Increments the Counter by 1. + Inc = 1, + /// Adds an arbitrary value to a Counter. (Returns an error if the value is < 0.) + /// + /// float + Add = 2, + /// Remove metrics for the given label values + Remove = 7, + /// Reset counter values + Reset = 8, + } + impl Operation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Operation::Unspecified => "OPERATION_UNSPECIFIED", + Operation::Inc => "OPERATION_INC", + Operation::Add => "OPERATION_ADD", + Operation::Remove => "OPERATION_REMOVE", + Operation::Reset => "OPERATION_RESET", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "OPERATION_UNSPECIFIED" => Some(Self::Unspecified), + "OPERATION_INC" => Some(Self::Inc), + "OPERATION_ADD" => Some(Self::Add), + "OPERATION_REMOVE" => Some(Self::Remove), + "OPERATION_RESET" => Some(Self::Reset), + _ => None, + } + } + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SummaryOp { + #[prost(enumeration="summary_op::Operation", tag="1")] + pub operation: i32, + /// Value (Float) to be used in the operation + #[prost(double, tag="2")] + pub value: f64, +} +/// Nested message and enum types in `SummaryOp`. +pub mod summary_op { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Operation { + /// Protobuf default should not be used, this is used so that the consume can ensure that the value was actually specified + Unspecified = 0, + /// Observe adds a single observation to the summary. + /// Observations are usually positive or zero. + /// Negative observations are accepted but prevent current versions of Prometheus from properly detecting counter resets in the sum of observations + Observe = 1, + /// Start a timer. Calling the returned function will observe the duration in seconds in the summary. + StartTimer = 2, + /// Remove metrics for the given label values + Remove = 7, + /// Reset counter values + Reset = 8, + } + impl Operation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Operation::Unspecified => "OPERATION_UNSPECIFIED", + Operation::Observe => "OPERATION_OBSERVE", + Operation::StartTimer => "OPERATION_START_TIMER", + Operation::Remove => "OPERATION_REMOVE", + Operation::Reset => "OPERATION_RESET", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "OPERATION_UNSPECIFIED" => Some(Self::Unspecified), + "OPERATION_OBSERVE" => Some(Self::Observe), + "OPERATION_START_TIMER" => Some(Self::StartTimer), + "OPERATION_REMOVE" => Some(Self::Remove), + "OPERATION_RESET" => Some(Self::Reset), + _ => None, + } + } + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct HistogramOp { + #[prost(enumeration="histogram_op::Operation", tag="1")] + pub operation: i32, + /// Value (Float) to be used in the operation + #[prost(double, tag="2")] + pub value: f64, +} +/// Nested message and enum types in `HistogramOp`. +pub mod histogram_op { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Operation { + /// Protobuf default should not be used, this is used so that the consume can ensure that the value was actually specified + Unspecified = 0, + /// Observe adds a single observation to the histogram. + /// Observations are usually positive or zero. + /// Negative observations are accepted but prevent current versions of Prometheus from properly detecting counter resets in the sum of observations. + Observe = 1, + /// Start a timer. Calling the returned function will observe the duration in seconds in the summary. + StartTimer = 2, + /// Initialize the metrics for the given combination of labels to zero + Zero = 3, + /// Remove metrics for the given label values + Remove = 7, + /// Reset counter values + Reset = 8, + } + impl Operation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Operation::Unspecified => "OPERATION_UNSPECIFIED", + Operation::Observe => "OPERATION_OBSERVE", + Operation::StartTimer => "OPERATION_START_TIMER", + Operation::Zero => "OPERATION_ZERO", + Operation::Remove => "OPERATION_REMOVE", + Operation::Reset => "OPERATION_RESET", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "OPERATION_UNSPECIFIED" => Some(Self::Unspecified), + "OPERATION_OBSERVE" => Some(Self::Observe), + "OPERATION_START_TIMER" => Some(Self::StartTimer), + "OPERATION_ZERO" => Some(Self::Zero), + "OPERATION_REMOVE" => Some(Self::Remove), + "OPERATION_RESET" => Some(Self::Reset), + _ => None, + } + } + } +} +// @@protoc_insertion_point(module) diff --git a/erc20-balances/src/pb/sf.substreams.sink.kv.v1.rs b/erc20-balances/src/pb/sf.substreams.sink.kv.v1.rs new file mode 100644 index 0000000..70b5bf8 --- /dev/null +++ b/erc20-balances/src/pb/sf.substreams.sink.kv.v1.rs @@ -0,0 +1,162 @@ +// @generated +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KvOperations { + #[prost(message, repeated, tag="1")] + pub operations: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KvOperation { + #[prost(string, tag="1")] + pub key: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="2")] + pub value: ::prost::alloc::vec::Vec, + #[prost(uint64, tag="3")] + pub ordinal: u64, + #[prost(enumeration="kv_operation::Type", tag="4")] + pub r#type: i32, +} +/// Nested message and enum types in `KVOperation`. +pub mod kv_operation { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Type { + /// Protobuf default should not be used, this is used so that the consume can ensure that the value was actually specified + Unset = 0, + Set = 1, + Delete = 2, + } + impl Type { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Type::Unset => "UNSET", + Type::Set => "SET", + Type::Delete => "DELETE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNSET" => Some(Self::Unset), + "SET" => Some(Self::Set), + "DELETE" => Some(Self::Delete), + _ => None, + } + } + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetRequest { + /// Key to fetch + #[prost(string, tag="1")] + pub key: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetManyRequest { + /// Keys to fetch + #[prost(string, repeated, tag="1")] + pub keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetByPrefixRequest { + /// server may impose a hard limit, trying to go above it would return grpc_error: INVALID_ARGUMENT + #[prost(uint64, tag="1")] + pub limit: u64, + /// requested prefix + #[prost(string, tag="2")] + pub prefix: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ScanRequest { + /// server may impose a hard limit, trying to go above it would return grpc_error: INVALID_ARGUMENT + #[prost(uint64, tag="1")] + pub limit: u64, + /// scanning will start at this point, lexicographically + #[prost(string, tag="2")] + pub begin: ::prost::alloc::string::String, + /// If set, scanning will stop when it reaches this point or above, excluding this exact key + #[prost(string, optional, tag="3")] + pub exclusive_end: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetResponse { + /// Value that was found for the requested key + #[prost(bytes="vec", tag="1")] + pub value: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetManyResponse { + /// Values that were found for the requested keys + #[prost(bytes="vec", repeated, tag="1")] + pub values: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetByPrefixResponse { + /// KV are the key/value pairs that were found with the given prefix + #[prost(message, repeated, tag="1")] + pub key_values: ::prost::alloc::vec::Vec, + /// limit_reached is true if there is at least ONE MORE result than the requested limit + #[prost(bool, tag="2")] + pub limit_reached: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ScanResponse { + /// KV are the key/value pairs that were found during scan + #[prost(message, repeated, tag="1")] + pub key_values: ::prost::alloc::vec::Vec, + /// limit_reached is true if there is at least ONE MORE result than the requested limit + #[prost(bool, tag="2")] + pub limit_reached: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Kv { + #[prost(string, tag="1")] + pub key: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="2")] + pub value: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Config { + #[prost(int64, tag="1")] + pub start_block: i64, + #[prost(string, tag="2")] + pub input_module: ::prost::alloc::string::String, +} +/// This defines a KV Sink to be queried with a generic key access interface (Get, GetMany, Scan, Prefix calls). +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenericService { + #[prost(message, optional, tag="1")] + pub sink_config: ::core::option::Option, +} +/// This defines configuration to run a WASM query service on top of the KV store being sync'd. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct WasmQueryService { + #[prost(message, optional, tag="1")] + pub sink_config: ::core::option::Option, + /// wasm exports: "kv_get_batch", "kv_get", "kv_scan", "kv_prefix" + #[prost(bytes="vec", tag="5")] + pub wasm_query_module: ::prost::alloc::vec::Vec, + /// Fully qualified Protobuf Service definition name + /// + /// sf.mycustom.v1.MyService + #[prost(string, tag="2")] + pub grpc_service: ::prost::alloc::string::String, +} +// @@protoc_insertion_point(module) diff --git a/erc20-balances/src/sinks.rs b/erc20-balances/src/sinks.rs new file mode 100644 index 0000000..e0c16d6 --- /dev/null +++ b/erc20-balances/src/sinks.rs @@ -0,0 +1,54 @@ +use crate::pb::erc20::types::v1::{BalanceChanges, Erc20Token}; +use crate::utils::helper::{append_0x, get_erc20_token}; +use substreams::scalar::BigInt; +use substreams::store::StoreGet; +use substreams::store::{StoreGetProto, StoreGetString}; +use substreams::{errors::Error, pb::substreams::Clock}; +use substreams_entity_change::pb::entity::EntityChanges; +use substreams_entity_change::tables::Tables; + +#[substreams::handlers::map] +pub fn graph_out( + clock: Clock, + block: BalanceChanges, + token: StoreGetString, +) -> Result { + let mut tables = Tables::new(); + let block_num = clock.number.to_string(); + let timestamp = clock.timestamp.unwrap().seconds.to_string(); + + for storage_change in block.balance_changes { + let token_lookup = token.get_last(&storage_change.contract); + let token_found = token_lookup.is_some(); + + if token_found { + let token = &get_erc20_token(storage_change.contract.clone()).unwrap(); + tables + .create_row("Token", append_0x(&storage_change.contract)) + .set("name", token.name.clone()) + .set("decimals", token.decimals.clone()) + .set("symbol", token.symbol.clone()); + } + + let id = format!("{}:{}", storage_change.contract, storage_change.owner); + + if storage_change.change_type == 0 { + continue; + } + + tables.create_row("Account", append_0x(&storage_change.owner.clone())); + + tables + .create_row("Balance", id) + // contract address + .set("token", append_0x(&storage_change.contract)) + // storage change + .set("owner", append_0x(&storage_change.owner)) + .set( + "balance", + BigInt::try_from(storage_change.new_balance).unwrap_or_default(), + ); + } + + Ok(tables.to_entity_changes()) +} diff --git a/erc20-balances/src/utils/helper.rs b/erc20-balances/src/utils/helper.rs new file mode 100644 index 0000000..adbca4a --- /dev/null +++ b/erc20-balances/src/utils/helper.rs @@ -0,0 +1,34 @@ + +use crate::abi; + +use abi::erc20::functions; +use substreams::scalar::BigInt; +use substreams::Hex; +use crate::pb::erc20::types::v1::Erc20Token; + + +pub fn append_0x(i: &str) -> String { + format!("0x{}", i) +} + +pub fn get_erc20_token(token_address: String) -> Option { + let token_address_vec = Hex::decode(token_address.clone()).unwrap(); + + let name = functions::Name {} + .call(token_address_vec.clone()) + .unwrap_or(String::new()); + let symbol = functions::Symbol {} + .call(token_address_vec.clone()) + .unwrap_or(String::new()); + let decimals = functions::Decimals {} + .call(token_address_vec.clone()) + .unwrap_or(BigInt::zero()) + .to_u64(); + + Some(Erc20Token { + address: token_address.clone(), + name: name, + symbol: symbol, + decimals: decimals, + }) +} \ No newline at end of file diff --git a/erc20-balances/src/utils/mod.rs b/erc20-balances/src/utils/mod.rs new file mode 100644 index 0000000..70c501d --- /dev/null +++ b/erc20-balances/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod helper; \ No newline at end of file diff --git a/erc20-balances/subgraph.yaml b/erc20-balances/subgraph.yaml new file mode 100644 index 0000000..4037b0d --- /dev/null +++ b/erc20-balances/subgraph.yaml @@ -0,0 +1,17 @@ +specVersion: 0.0.6 +description: ERC20 Ethereum (powered by Substreams) +repository: https://github.com/GundamDweeb/substreams-erc20 +schema: + file: ./schema.graphql + +dataSources: + - kind: substreams + name: erc20 + network: mainnet + source: + package: + moduleName: graph_out + file: ./erc20-v0.3.0.spkg + mapping: + kind: substreams/graph-entities + apiVersion: 0.0.7 diff --git a/erc20-balances/substreams.yaml b/erc20-balances/substreams.yaml new file mode 100644 index 0000000..6432272 --- /dev/null +++ b/erc20-balances/substreams.yaml @@ -0,0 +1,49 @@ +specVersion: v0.1.0 +package: + name: erc20 + version: v0.3.0 + url: https://github.com/GundamDweeb/substreams-erc20 + doc: ERC-20 + +imports: + entities: https://github.com/streamingfast/substreams-sink-entity-changes/releases/download/v1.3.0/substreams-sink-entity-changes-v1.3.0.spkg + +binaries: + default: + type: wasm/rust-v1 + file: ./target/wasm32-unknown-unknown/release/erc20.wasm + +protobuf: + files: + - erc20.proto + importPaths: + - ./proto/v1 + +modules: + - name: map_block + kind: map + initialBlock: 447766 + doc: Storage changes from 'transfer' and 'transferFrom' functions. + inputs: + - source: sf.ethereum.type.v2.Block + output: + type: proto:erc20.types.v1.BalanceChanges + + - name: store_tokens + kind: store + initialBlock: 447766 + updatePolicy: set_if_not_exists + valueType: string + inputs: + - map: map_block + + + - name: graph_out + kind: map + initialBlock: 447766 + inputs: + - source: sf.substreams.v1.Clock + - map: map_block + - store: store_tokens + output: + type: proto:sf.substreams.sink.entity.v1.EntityChanges \ No newline at end of file