diff --git a/Cargo.lock b/Cargo.lock index 53aec903..35dd642b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,6 +1290,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "substreams-pancake-v2" +version = "0.1.0" +dependencies = [ + "anyhow", + "ethabi", + "hex", + "hex-literal", + "pad", + "prost 0.11.9", + "substreams 0.5.10", + "substreams-common", + "substreams-entity-change 1.3.0", + "substreams-ethereum", + "substreams-helper", +] + [[package]] name = "substreams-solana" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index eedf6a7e..926d00ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "eth-supply", "synthetix", "aave-v2", + "pancake-v2", ] exclude = ["messari-cli"] diff --git a/pancake-v2/Cargo.toml b/pancake-v2/Cargo.toml new file mode 100644 index 00000000..b7d61f34 --- /dev/null +++ b/pancake-v2/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "substreams-pancake-v2" +version = "0.1.0" +description = "Messari's standardized substream for Pancake v2" +edition = "2021" +repository = "https://github.com/messari/substreams/" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ethabi = "17.0" +hex = "0.4.3" +hex-literal = "0.3.4" +prost = "^0.11.0" +pad = "0.1" +substreams.workspace = true +substreams-ethereum.workspace = true +substreams-helper = { path = "../substreams-helper" } +substreams-entity-change = "1.3.0" + +[build-dependencies] +anyhow = "1" +substreams-common = { path = "../common" } diff --git a/pancake-v2/Makefile b/pancake-v2/Makefile new file mode 100644 index 00000000..3cb316fb --- /dev/null +++ b/pancake-v2/Makefile @@ -0,0 +1,17 @@ +.PHONY: build_all +build_all: + $(MAKE) -C ../erc20-price build + $(MAKE) -C ../erc20-price pack + cargo build --target wasm32-unknown-unknown --release + +.PHONY: build +build: + cargo build --target wasm32-unknown-unknown --release + +.PHONY: run +run: + substreams run -e bnb.streamingfast.io:443 substreams.yaml graph_out -s 6809737 + +.PHONY: pack +pack: + substreams pack ./substreams.yaml diff --git a/pancake-v2/abi/ERC20.json b/pancake-v2/abi/ERC20.json new file mode 100644 index 00000000..405d6b36 --- /dev/null +++ b/pancake-v2/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" + } +] diff --git a/pancake-v2/abi/Factory.json b/pancake-v2/abi/Factory.json new file mode 100644 index 00000000..1b14599d --- /dev/null +++ b/pancake-v2/abi/Factory.json @@ -0,0 +1,125 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_feeToSetter", "type": "address" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "PairCreated", + "type": "event" + }, + { + "constant": true, + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "allPairs", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "allPairsLength", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "tokenA", "type": "address" }, + { "internalType": "address", "name": "tokenB", "type": "address" } + ], + "name": "createPair", + "outputs": [ + { "internalType": "address", "name": "pair", "type": "address" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "feeTo", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "feeToSetter", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "getPair", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "_feeTo", "type": "address" } + ], + "name": "setFeeTo", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "_feeToSetter", "type": "address" } + ], + "name": "setFeeToSetter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/pancake-v2/abi/Pool.json b/pancake-v2/abi/Pool.json new file mode 100644 index 00000000..53582c1e --- /dev/null +++ b/pancake-v2/abi/Pool.json @@ -0,0 +1,713 @@ +[ + { + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint112", + "name": "reserve0", + "type": "uint112" + }, + { + "indexed": false, + "internalType": "uint112", + "name": "reserve1", + "type": "uint112" + } + ], + "name": "Sync", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MINIMUM_LIQUIDITY", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "burn", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getReserves", + "outputs": [ + { + "internalType": "uint112", + "name": "_reserve0", + "type": "uint112" + }, + { + "internalType": "uint112", + "name": "_reserve1", + "type": "uint112" + }, + { + "internalType": "uint32", + "name": "_blockTimestampLast", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_token0", + "type": "address" + }, + { + "internalType": "address", + "name": "_token1", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "kLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "price0CumulativeLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "price1CumulativeLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "skim", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "sync", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/pancake-v2/build.rs b/pancake-v2/build.rs new file mode 100644 index 00000000..fb321d31 --- /dev/null +++ b/pancake-v2/build.rs @@ -0,0 +1,10 @@ +use anyhow::{Ok, Result}; +use substreams_common::codegen; + +fn main() -> Result<(), anyhow::Error> { + println!("cargo:rerun-if-changed=proto"); + println!("cargo:rerun-if-changed=abi"); + codegen::generate(None)?; + + Ok(()) +} diff --git a/pancake-v2/proto/v1/pancake.proto b/pancake-v2/proto/v1/pancake.proto new file mode 100644 index 00000000..ceee368b --- /dev/null +++ b/pancake-v2/proto/v1/pancake.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +import "erc20.proto"; +package messari.pancake.v2; + +message Pools { + repeated Pool pools = 1; +} + +message Pool { + string name = 1; + string symbol = 2; + string address = 3; + + messari.erc20.v1.ERC20Tokens input_tokens = 4; + messari.erc20.v1.ERC20Token output_token = 5; + + int64 created_timestamp = 6; + int64 created_block_number = 7; +} + +message Events { + repeated Event events = 1; +} + +message Event { + oneof type { + DepositEvent deposit_type = 10; + WithdrawEvent withdraw_type = 20; + SyncEvent sync_type = 30; + SwapEvent swap_type = 40; + } + + string hash = 100; + uint32 log_index = 101; + uint64 log_ordinal = 102; + string to = 103; + string from = 104; + uint64 block_number = 105; + uint64 timestamp = 106; + string pool = 107; +} + +message DepositEvent { + repeated string input_token_amounts = 1; + optional string output_token_amount = 2; +} + +message WithdrawEvent { + repeated string input_token_amounts = 1; + optional string output_token_amount = 2; +} + +message SyncEvent { + string reserve0 = 1; + string reserve1 = 2; +} + +message SwapEvent { + messari.erc20.v1.ERC20Token token_in = 1; + string amount_in = 2; + + messari.erc20.v1.ERC20Token token_out = 3; + string amount_out = 4; +} diff --git a/pancake-v2/rust-toolchain.toml b/pancake-v2/rust-toolchain.toml new file mode 100644 index 00000000..0a0b90ae --- /dev/null +++ b/pancake-v2/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.64.0" +components = [ "rustfmt" ] +targets = [ "wasm32-unknown-unknown" ] diff --git a/pancake-v2/schema.graphql b/pancake-v2/schema.graphql new file mode 100644 index 00000000..b7a8facd --- /dev/null +++ b/pancake-v2/schema.graphql @@ -0,0 +1,807 @@ +# Subgraph Schema: DEX AMM +# Version: 1.3.1 +# See https://github.com/messari/subgraphs/blob/master/docs/SCHEMA.md for details + +enum Network { + ARBITRUM_ONE + ARWEAVE_MAINNET + AURORA + AVALANCHE + BOBA + BSC # aka BNB Chain + CELO + COSMOS + CRONOS + MAINNET # Ethereum Mainnet + FANTOM + FUSE + HARMONY + JUNO + MOONBEAM + MOONRIVER + NEAR_MAINNET + OPTIMISM + OSMOSIS + MATIC # aka Polygon + GNOSIS +} + +enum ProtocolType { + EXCHANGE + LENDING + YIELD + BRIDGE + GENERIC + # Will add more +} + +type Token @entity { + " Smart contract address of the token " + id: ID! + + " Name of the token, mirrored from the smart contract " + name: String! + + " Symbol of the token, mirrored from the smart contract " + symbol: String! + + " The number of decimal places this token uses, default to 18 " + decimals: Int! + + " Optional field to track the price of a token, mostly for caching purposes " + lastPriceUSD: BigDecimal + + " Optional field to track the block number of the last token price " + lastPriceBlockNumber: BigInt + + " Optional field to track the block number of the last token price " + whitelistedPools: [String!] +} + +enum RewardTokenType { + " For reward tokens awarded to LPs/lenders " + DEPOSIT + + " For reward tokens awarded to borrowers " + BORROW +} + +type RewardToken @entity { + " { Reward token type }-{ Smart contract address of the reward token } " + id: ID! + + " Reference to the actual token " + token: Token! + + " The type of the reward token " + type: RewardTokenType! +} + +# Note that trading fee is the fee paid *by* the users, whereas LP fee and +# protocol fee are fees paid *to* the corresponding parties. +# Most of the time, trading fee = liquidity provider fee + protocol fee, +# but not always. Thus we explicitly specify all different fee types. +enum LiquidityPoolFeeType { + " Total fixed fee paid by the user per trade, as a percentage of the traded amount. e.g. 0.3% for Uniswap v2, 0.3% for Sushiswap, 0.04% for Curve v1. " + FIXED_TRADING_FEE + + " Some protocols use tiered fees instead of fixed fee (e.g. DYDX, DODO). Set `feePercentage` as 0 but handle the tiered fees in the mapping code. " + TIERED_TRADING_FEE + + " Some protocols use dynamic fees instead of fixed fee (e.g. Balancer v2). Set `feePercentage` as 0 but handle the dynamic fees in the mapping code. " + DYNAMIC_TRADING_FEE + + " Fixed fee that's paid to the LP, as a percentage of the traded amount. e.g. 0.25% for Sushiswap, 0.02% for Curve v1. " + FIXED_LP_FEE + + " Some protocols use dynamic LP fees (e.g., Bancor v2). Set `feePercentage` as 0 but handle the dynamic fees in the mapping code. " + DYNAMIC_LP_FEE + + " Fixed fee that's paid to the protocol, as a percentage of the traded amount. e.g. 0.05% for Sushiswap, 0.02% for Curve v1. " + FIXED_PROTOCOL_FEE + + " Some protocols use dynamic protocol fees (e.g., Bancor v2). Set `feePercentage` as 0 but handle the dynamic fees in the mapping code. " + DYNAMIC_PROTOCOL_FEE + + " One-time fee charged by the protocol during deposit, in percentages of the deposit token " + DEPOSIT_FEE + + " One-time fee charged by the protocol (e.g. Bancor v3) during withdrawal, in percentages of the withdrawal token " + WITHDRAWAL_FEE +} + +type LiquidityPoolFee @entity { + " { Fee type }-{ Pool address } " + id: ID! + + " Fee as a percentage of the trade (swap) amount. Does not always apply " + feePercentage: BigDecimal + + " Type of fee this pool uses " + feeType: LiquidityPoolFeeType! +} + +############################# +##### Protocol Metadata ##### +############################# + +interface Protocol { + " Smart contract address of the protocol's main contract (Factory, Registry, etc) " + id: ID! + + " Name of the protocol, including version. e.g. Uniswap v3 " + name: String! + + " Slug of protocol, including version. e.g. uniswap-v3 " + slug: String! + + " Version of the subgraph schema, in SemVer format (e.g. 1.0.0) " + schemaVersion: String! + + " Version of the subgraph implementation, in SemVer format (e.g. 1.0.0) " + subgraphVersion: String! + + " Version of the methodology used to compute metrics, loosely based on SemVer format (e.g. 1.0.0) " + methodologyVersion: String! + + " The blockchain network this subgraph is indexing on " + network: Network! + + " The type of protocol (e.g. DEX, Lending, Yield, etc) " + type: ProtocolType! + + ##### Quantitative Data ##### + + " Current TVL (Total Value Locked) of the entire protocol " + totalValueLockedUSD: BigDecimal! + + " Current PCV (Protocol Controlled Value). Only relevant for protocols with PCV. " + protocolControlledValueUSD: BigDecimal + + " Revenue claimed by suppliers to the protocol. LPs on DEXs (e.g. 0.25% of the swap fee in Sushiswap). Depositors on Lending Protocols. NFT sellers on OpenSea. " + cumulativeSupplySideRevenueUSD: BigDecimal! + + " Gross revenue for the protocol (revenue claimed by protocol). Examples: AMM protocol fee (Sushi’s 0.05%). OpenSea 10% sell fee. " + cumulativeProtocolSideRevenueUSD: BigDecimal! + + " All revenue generated by the protocol. e.g. 0.30% of swap fee in Sushiswap, all yield generated by Yearn. " + cumulativeTotalRevenueUSD: BigDecimal! + + " Number of cumulative unique users " + cumulativeUniqueUsers: Int! + + " Total number of pools " + totalPoolCount: Int! + + ##### Snapshots ##### + + " Daily usage metrics for this protocol " + dailyUsageMetrics: [UsageMetricsDailySnapshot!]! + @derivedFrom(field: "protocol") + + " Hourly usage metrics for this protocol " + hourlyUsageMetrics: [UsageMetricsHourlySnapshot!]! + @derivedFrom(field: "protocol") + + " Daily financial metrics for this protocol " + financialMetrics: [FinancialsDailySnapshot!]! @derivedFrom(field: "protocol") +} + +type DexAmmProtocol implements Protocol @entity { + " Smart contract address of the protocol's main contract (Factory, Registry, etc) " + id: ID! + + " Name of the protocol, including version. e.g. Uniswap v3 " + name: String! + + " Slug of protocol, including version. e.g. uniswap-v3 " + slug: String! + + " Version of the subgraph schema, in SemVer format (e.g. 1.0.0) " + schemaVersion: String! + + " Version of the subgraph implementation, in SemVer format (e.g. 1.0.0) " + subgraphVersion: String! + + " Version of the methodology used to compute metrics, loosely based on SemVer format (e.g. 1.0.0) " + methodologyVersion: String! + + " The blockchain network this subgraph is indexing on " + network: Network! + + " The type of protocol (e.g. DEX, Lending, Yield, etc) " + type: ProtocolType! + + ##### Quantitative Data ##### + + " Current TVL (Total Value Locked) of the entire protocol " + totalValueLockedUSD: BigDecimal! + + " Current PCV (Protocol Controlled Value). Only relevant for protocols with PCV. " + protocolControlledValueUSD: BigDecimal + + " All historical volume in USD " + cumulativeVolumeUSD: BigDecimal! + + " Revenue claimed by suppliers to the protocol. LPs on DEXs (e.g. 0.25% of the swap fee in Sushiswap). Depositors on Lending Protocols. NFT sellers on OpenSea. " + cumulativeSupplySideRevenueUSD: BigDecimal! + + " Gross revenue for the protocol (revenue claimed by protocol). Examples: AMM protocol fee (Sushi’s 0.05%). OpenSea 10% sell fee. " + cumulativeProtocolSideRevenueUSD: BigDecimal! + + " All revenue generated by the protocol. e.g. 0.30% of swap fee in Sushiswap, all yield generated by Yearn. " + cumulativeTotalRevenueUSD: BigDecimal! + + " Number of cumulative unique users " + cumulativeUniqueUsers: Int! + + " Total number of pools " + totalPoolCount: Int! + + ##### Snapshots ##### + + " Daily usage metrics for this protocol " + dailyUsageMetrics: [UsageMetricsDailySnapshot!]! + @derivedFrom(field: "protocol") + + " Hourly usage metrics for this protocol " + hourlyUsageMetrics: [UsageMetricsHourlySnapshot!]! + @derivedFrom(field: "protocol") + + " Daily financial metrics for this protocol " + financialMetrics: [FinancialsDailySnapshot!]! @derivedFrom(field: "protocol") + + ##### Pools ##### + + " All pools that belong to this protocol " + pools: [LiquidityPool!]! @derivedFrom(field: "protocol") +} + +############################### +##### Protocol Timeseries ##### +############################### + +type UsageMetricsDailySnapshot @entity { + " ID is # of days since Unix epoch time " + id: ID! + + " Protocol this snapshot is associated with " + protocol: DexAmmProtocol! + + " Number of unique daily active users " + dailyActiveUsers: Int! + + " Number of cumulative unique users " + cumulativeUniqueUsers: Int! + + " Total number of transactions occurred in a day. Transactions include all entities that implement the Event interface. " + dailyTransactionCount: Int! + + " Total number of deposits (add liquidity) in a day " + dailyDepositCount: Int! + + " Total number of withdrawals (remove liquidity) in a day " + dailyWithdrawCount: Int! + + " Total number of trades (swaps) in a day " + dailySwapCount: Int! + + " Total number of pools " + totalPoolCount: Int! + + " Block number of this snapshot " + blockNumber: BigInt! + + " Timestamp of this snapshot " + timestamp: BigInt! +} + +type UsageMetricsHourlySnapshot @entity { + " { # of hours since Unix epoch time } " + id: ID! + + " Protocol this snapshot is associated with " + protocol: DexAmmProtocol! + + " Number of unique hourly active users " + hourlyActiveUsers: Int! + + " Number of cumulative unique users " + cumulativeUniqueUsers: Int! + + " Total number of transactions occurred in an hour. Transactions include all entities that implement the Event interface. " + hourlyTransactionCount: Int! + + " Total number of deposits (add liquidity) in an hour " + hourlyDepositCount: Int! + + " Total number of withdrawals (remove liquidity) in an hour " + hourlyWithdrawCount: Int! + + " Total number of trades (swaps) in an hour " + hourlySwapCount: Int! + + " Block number of this snapshot " + blockNumber: BigInt! + + " Timestamp of this snapshot " + timestamp: BigInt! +} + +type FinancialsDailySnapshot @entity { + " ID is # of days since Unix epoch time " + id: ID! + + " Protocol this snapshot is associated with " + protocol: DexAmmProtocol! + + " Current TVL (Total Value Locked) of the entire protocol " + totalValueLockedUSD: BigDecimal! + + " Current PCV (Protocol Controlled Value). Only relevant for protocols with PCV. " + protocolControlledValueUSD: BigDecimal + + " All trade volume occurred in a given day, in USD " + dailyVolumeUSD: BigDecimal! + + " All historical trade volume in USD " + cumulativeVolumeUSD: BigDecimal! + + " Revenue claimed by suppliers to the protocol. LPs on DEXs (e.g. 0.25% of the swap fee in Sushiswap). Depositors on Lending Protocols. NFT sellers on OpenSea. " + dailySupplySideRevenueUSD: BigDecimal! + + " Revenue claimed by suppliers to the protocol. LPs on DEXs (e.g. 0.25% of the swap fee in Sushiswap). Depositors on Lending Protocols. NFT sellers on OpenSea. " + cumulativeSupplySideRevenueUSD: BigDecimal! + + " Gross revenue for the protocol (revenue claimed by protocol). Examples: AMM protocol fee (Sushi’s 0.05%). OpenSea 10% sell fee. " + dailyProtocolSideRevenueUSD: BigDecimal! + + " Gross revenue for the protocol (revenue claimed by protocol). Examples: AMM protocol fee (Sushi’s 0.05%). OpenSea 10% sell fee. " + cumulativeProtocolSideRevenueUSD: BigDecimal! + + " All revenue generated by the protocol. e.g. 0.30% of swap fee in Sushiswap, all yield generated by Yearn. " + dailyTotalRevenueUSD: BigDecimal! + + " All revenue generated by the protocol. e.g. 0.30% of swap fee in Sushiswap, all yield generated by Yearn. " + cumulativeTotalRevenueUSD: BigDecimal! + + " Block number of this snapshot " + blockNumber: BigInt! + + " Timestamp of this snapshot " + timestamp: BigInt! +} + +############################### +##### Pool-Level Metadata ##### +############################### + +type LiquidityPool @entity { + " Smart contract address of the pool " + id: ID! + + " The protocol this pool belongs to " + protocol: DexAmmProtocol! + + " Name of liquidity pool (e.g. Curve.fi DAI/USDC/USDT) " + name: String + + " Symbol of liquidity pool (e.g. 3CRV) " + symbol: String + + " Tokens that need to be deposited to take a position in protocol. e.g. WETH and USDC to deposit into the WETH-USDC pool. Array to account for multi-asset pools like Curve and Balancer " + inputTokens: [Token!]! + + " Token that is minted to track ownership of position in protocol " + outputToken: Token + + " Aditional tokens that are given as reward for position in a protocol, usually in liquidity mining programs. e.g. SUSHI in the Onsen program, MATIC for Aave Polygon, usually in liquidity mining programs. e.g. SUSHI in the Onsen program, MATIC for Aave Polygon " + rewardTokens: [RewardToken!] + + " Fees per trade incurred to the user. Should include all fees that apply to a pool (e.g. Curve has a trading fee AND an admin fee, which is a portion of the trading fee. Uniswap only has a trading fee and no protocol fee. ) " + fees: [LiquidityPoolFee!]! + + " Whether this pool is single-sided (e.g. Bancor, Platypus's Alternative Pool). The specifics of the implementation depends on the protocol. " + isSingleSided: Boolean! + + " Creation timestamp " + createdTimestamp: BigInt! + + " Creation block number " + createdBlockNumber: BigInt! + + ##### Quantitative Data ##### + + " Current TVL (Total Value Locked) of this pool in USD " + totalValueLockedUSD: BigDecimal! + + " All revenue generated by the liquidity pool, accrued to the supply side. " + cumulativeSupplySideRevenueUSD: BigDecimal! + + " All revenue generated by the liquidity pool, accrued to the protocol. " + cumulativeProtocolSideRevenueUSD: BigDecimal! + + " All revenue generated by the liquidity pool. " + cumulativeTotalRevenueUSD: BigDecimal! + + " All historical trade volume occurred in this pool, in USD " + cumulativeVolumeUSD: BigDecimal! + + " Amount of input tokens in the pool. The ordering should be the same as the pool's `inputTokens` field. " + inputTokenBalances: [String!]! + + " Weights of input tokens in the liquidity pool in percentage values. For example, 50/50 for Uniswap pools, 48.2/51.8 for a Curve pool, 10/10/80 for a Balancer pool " + inputTokenWeights: [String!]! + + " Total supply of output token. Note that certain DEXes don't have an output token (e.g. Bancor) " + outputTokenSupply: BigInt + + " Price per share of output token in USD " + outputTokenPriceUSD: BigDecimal + + " Total supply of output tokens that are staked (usually in the MasterChef contract). Used to calculate reward APY. " + stakedOutputTokenAmount: BigInt + + " Per-block reward token emission as of the current block normalized to a day, in token's native amount. This should be ideally calculated as the theoretical rate instead of the realized amount. " + rewardTokenEmissionsAmount: [BigInt!] + + " Per-block reward token emission as of the current block normalized to a day, in USD value. This should be ideally calculated as the theoretical rate instead of the realized amount. " + rewardTokenEmissionsUSD: [BigDecimal!] + + ##### Snapshots ##### + + " Liquidity pool daily snapshots " + dailySnapshots: [LiquidityPoolDailySnapshot!]! @derivedFrom(field: "pool") + + " Liquidity pool hourly snapshots " + hourlySnapshots: [LiquidityPoolHourlySnapshot!]! @derivedFrom(field: "pool") + + ##### Events ##### + + " All deposit (add liquidity) events occurred in this pool " + deposits: [Deposit!]! @derivedFrom(field: "pool") + + " All withdraw (remove liquidity) events occurred in this pool " + withdraws: [Withdraw!]! @derivedFrom(field: "pool") + + " All trade (swap) events occurred in this pool " + swaps: [Swap!]! @derivedFrom(field: "pool") +} + +################################# +##### Pool-Level Timeseries ##### +################################# + +type LiquidityPoolDailySnapshot @entity { + " { Smart contract address of the pool }-{ # of days since Unix epoch time } " + id: ID! + + " The protocol this snapshot belongs to " + protocol: DexAmmProtocol! + + " The pool this snapshot belongs to " + pool: LiquidityPool! + + " Block number of this snapshot " + blockNumber: BigInt! + + " Timestamp of this snapshot " + timestamp: BigInt! + + ##### Quantitative Data ##### + + " Current TVL (Total Value Locked) of this pool " + totalValueLockedUSD: BigDecimal! + + " All revenue generated by the liquidity pool, accrued to the supply side. " + cumulativeSupplySideRevenueUSD: BigDecimal! + + " Daily revenue generated by the liquidity pool, accrued to the supply side. " + dailySupplySideRevenueUSD: BigDecimal! + + " All revenue generated by the liquidity pool, accrued to the protocol. " + cumulativeProtocolSideRevenueUSD: BigDecimal! + + " Daily revenue generated by the liquidity pool, accrued to the protocol. " + dailyProtocolSideRevenueUSD: BigDecimal! + + " All revenue generated by the liquidity pool. " + cumulativeTotalRevenueUSD: BigDecimal! + + " Daily revenue generated by the liquidity pool. " + dailyTotalRevenueUSD: BigDecimal! + + " All trade volume occurred in a given day, in USD " + dailyVolumeUSD: BigDecimal! + + " All trade volume occurred in a given day for a specific input token, in native amount. The ordering should be the same as the pool's `inputTokens` field. " + dailyVolumeByTokenAmount: [String!]! + + " All trade volume occurred in a given day for a specific input token, in USD. The ordering should be the same as the pool's `inputTokens` field. " + dailyVolumeByTokenUSD: [String!]! + + " All historical trade volume occurred in this pool, in USD " + cumulativeVolumeUSD: BigDecimal! + + " Amount of input tokens in the pool. The ordering should be the same as the pool's `inputTokens` field. " + inputTokenBalances: [String!]! + + " Weights of input tokens in the liquidity pool in percentage values. For example, 50/50 for Uniswap pools, 48.2/51.8 for a Curve pool, 10/10/80 for a Balancer pool " + inputTokenWeights: [String!]! + + " Total supply of output token. Note that certain DEXes don't have an output token (e.g. Bancor) " + outputTokenSupply: BigInt + + " Price per share of output token in USD " + outputTokenPriceUSD: BigDecimal + + " Total supply of output tokens that are staked (usually in the MasterChef contract). Used to calculate reward APY. " + stakedOutputTokenAmount: BigInt + + " Per-block reward token emission as of the current block normalized to a day, in token's native amount. This should be ideally calculated as the theoretical rate instead of the realized amount. " + rewardTokenEmissionsAmount: [BigInt!] + + " Per-block reward token emission as of the current block normalized to a day, in USD value. This should be ideally calculated as the theoretical rate instead of the realized amount. " + rewardTokenEmissionsUSD: [BigDecimal!] + + _inputTokenPrices: [String!] +} + +type LiquidityPoolHourlySnapshot @entity { + " { Smart contract address of the pool }-{ # of hours since Unix epoch time } " + id: ID! + + " The protocol this snapshot belongs to " + protocol: DexAmmProtocol! + + " The pool this snapshot belongs to " + pool: LiquidityPool! + + " Block number of this snapshot " + blockNumber: BigInt! + + " Timestamp of this snapshot " + timestamp: BigInt! + + ##### Quantitative Data ##### + + " Current TVL (Total Value Locked) of this pool " + totalValueLockedUSD: BigDecimal! + + " All revenue generated by the liquidity pool, accrued to the supply side. " + cumulativeSupplySideRevenueUSD: BigDecimal! + + " Hourly revenue generated by the liquidity pool, accrued to the supply side. " + hourlySupplySideRevenueUSD: BigDecimal! + + " All revenue generated by the liquidity pool, accrued to the protocol. " + cumulativeProtocolSideRevenueUSD: BigDecimal! + + " Hourly revenue generated by the liquidity pool, accrued to the protocol. " + hourlyProtocolSideRevenueUSD: BigDecimal! + + " All revenue generated by the liquidity pool. " + cumulativeTotalRevenueUSD: BigDecimal! + + " Hourly revenue generated by the liquidity pool. " + hourlyTotalRevenueUSD: BigDecimal! + + " All trade volume occurred in a given hour, in USD " + hourlyVolumeUSD: BigDecimal! + + " All trade volume occurred in a given hour for a specific input token, in native amount. The ordering should be the same as the pool's `inputTokens` field. " + hourlyVolumeByTokenAmount: [String!]! + + " All trade volume occurred in a given hour for a specific input token, in USD. The ordering should be the same as the pool's `inputTokens` field. " + hourlyVolumeByTokenUSD: [String!]! + + " All historical trade volume occurred in this pool, in USD " + cumulativeVolumeUSD: BigDecimal! + + " Amount of input tokens in the pool. The ordering should be the same as the pool's `inputTokens` field. " + inputTokenBalances: [String!]! + + " Weights of input tokens in the liquidity pool in percentage values. For example, 50/50 for Uniswap pools, 48.2/51.8 for a Curve pool, 10/10/80 for a Balancer pool " + inputTokenWeights: [String!]! + + " Total supply of output token. Note that certain DEXes don't have an output token (e.g. Bancor) " + outputTokenSupply: BigInt + + " Price per share of output token in USD " + outputTokenPriceUSD: BigDecimal + + " Total supply of output tokens that are staked (usually in the MasterChef contract). Used to calculate reward APY. " + stakedOutputTokenAmount: BigInt + + " Per-block reward token emission as of the current block normalized to a day (not hour), in token's native amount. This should be ideally calculated as the theoretical rate instead of the realized amount. " + rewardTokenEmissionsAmount: [BigInt!] + + " Per-block reward token emission as of the current block normalized to a day (not hour), in USD value. This should be ideally calculated as the theoretical rate instead of the realized amount. " + rewardTokenEmissionsUSD: [BigDecimal!] +} + +################################## +##### Transaction-Level Data ##### +################################## + +""" +An event is any user action that occurs in a protocol. Generally, they are Ethereum events +emitted by a function in the smart contracts, stored in transaction receipts as event logs. +However, some user actions of interest are function calls that don't emit events. For example, +the deposit and withdraw functions in Yearn do not emit any events. In our subgraphs, we still +store them as events, although they are not technically Ethereum events emitted by smart +contracts. +""" +interface Event { + " { Event type }-{ Transaction hash }-{ Log index } " + id: ID! + + " Transaction hash of the transaction that emitted this event " + hash: String! + + " Event log index. For transactions that don't emit event, create arbitrary index starting from 0 " + logIndex: Int! + + " The protocol this transaction belongs to " + protocol: DexAmmProtocol! + + " Address that received the tokens " + to: String! + + " Address that sent the tokens " + from: String! + + " Block number of this event " + blockNumber: BigInt! + + " Timestamp of this event " + timestamp: BigInt! +} + +type Deposit implements Event @entity { + " deposit-{ Transaction hash }-{ Log index } " + id: ID! + + " Transaction hash of the transaction that emitted this event " + hash: String! + + " Event log index. For transactions that don't emit event, create arbitrary index starting from 0 " + logIndex: Int! + + " The protocol this transaction belongs to " + protocol: DexAmmProtocol! + + " Address that received the tokens " + to: String! + + " Address that sent the tokens " + from: String! + + " Block number of this event " + blockNumber: BigInt! + + " Timestamp of this event " + timestamp: BigInt! + + " Input tokens of the pool. E.g. WETH and USDC to a WETH-USDC pool " + inputTokens: [Token!]! + + " Output token of the pool. E.g. the UNI-LP token " + outputToken: Token + + " Amount of input tokens in the token's native unit " + inputTokenAmounts: [String!]! + + " Amount of output tokens in the token's native unit " + outputTokenAmount: BigInt + + " USD-normalized value of the transaction of the underlying (e.g. sum of tokens deposited into a pool) " + amountUSD: BigDecimal! + + " The pool involving this transaction " + pool: LiquidityPool! +} + +type Withdraw implements Event @entity { + " withdraw-{ Transaction hash }-{ Log index }" + id: ID! + + " Transaction hash of the transaction that emitted this event " + hash: String! + + " Event log index. For transactions that don't emit event, create arbitrary index starting from 0 " + logIndex: Int! + + " The protocol this transaction belongs to " + protocol: DexAmmProtocol! + + " Address that received the tokens " + to: String! + + " Address that sent the tokens " + from: String! + + " Block number of this event " + blockNumber: BigInt! + + " Timestamp of this event " + timestamp: BigInt! + + " Input tokens of the pool (not input tokens of the event/transaction). E.g. WETH and USDC from a WETH-USDC pool " + inputTokens: [Token!]! + + " Output token of the pool (not output token of the event/transaction). E.g. the UNI-LP token " + outputToken: Token + + " Amount of input tokens in the token's native unit " + inputTokenAmounts: [String!]! + + " Amount of output tokens in the token's native unit " + outputTokenAmount: BigInt + + " USD-normalized value of the transaction of the underlying (e.g. sum of tokens withdrawn from a pool) " + amountUSD: BigDecimal! + + " The pool involving this transaction " + pool: LiquidityPool! +} + +type Swap implements Event @entity { + " swap-{ Transaction hash }-{ Log index } " + id: ID! + + " Transaction hash of the transaction that emitted this event " + hash: String! + + " Event log index. For transactions that don't emit event, create arbitrary index starting from 0 " + logIndex: Int! + + " The protocol this transaction belongs to " + protocol: DexAmmProtocol! + + " Address that received the tokens " + to: String! + + " Address that sent the tokens " + from: String! + + " Block number of this event " + blockNumber: BigInt! + + " Timestamp of this event " + timestamp: BigInt! + + " Token deposited into pool " + tokenIn: Token! + + " Amount of token deposited into pool in native units " + amountIn: BigInt! + + " Amount of token deposited into pool in USD " + amountInUSD: BigDecimal! + + " Token withdrawn from pool " + tokenOut: Token! + + " Amount of token withdrawn from pool in native units " + amountOut: BigInt! + + " Amount of token withdrawn from pool in USD " + amountOutUSD: BigDecimal! + + " The pool involving this transaction " + pool: LiquidityPool! +} + +# An account is a unique Ethereum address +# Helps to accumulate total unique users +type Account @entity { + " Address of the account " + id: ID! +} + +# Helper entity for calculating daily/hourly active users +type ActiveAccount @entity { + " { daily/hourly }-{ Address of the account }-{ Days/hours since Unix epoch } " + id: ID! +} diff --git a/pancake-v2/src/abi.rs b/pancake-v2/src/abi.rs new file mode 100644 index 00000000..4c55c1b9 --- /dev/null +++ b/pancake-v2/src/abi.rs @@ -0,0 +1,15 @@ +// DO NOT EDIT - the file is generated by build script +#[rustfmt::skip] +#[allow(unused_imports)] +#[path = "../target/abi/ERC20.rs"] +pub mod ERC20; + +#[rustfmt::skip] +#[allow(unused_imports)] +#[path = "../target/abi/Factory.rs"] +pub mod Factory; + +#[rustfmt::skip] +#[allow(unused_imports)] +#[path = "../target/abi/Pool.rs"] +pub mod Pool; diff --git a/pancake-v2/src/common/constants.rs b/pancake-v2/src/common/constants.rs new file mode 100644 index 00000000..d9fda741 --- /dev/null +++ b/pancake-v2/src/common/constants.rs @@ -0,0 +1,33 @@ +pub const WETH_ADDRESS: &str = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; +pub const USDC_ADDRESS: &str = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; + +pub const PANCAKE_V2_FACTORY: &str = "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73"; + +pub const STABLE_COINS: [&str; 5] = [ + "0x55d398326f99059ff775485246999027b3197955", // BUSD + "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", // USDC + "0xe9e7cea3dedca5984780bafc599bd69add087d56", // USDT + "0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3", // DAI + "0x40af3827F39D0EAcBF4A168f8D4ee67c121D11c9", // TUSD +]; + +pub const PAIR_COINS: [&str; 4] = [ + "0x2170ed0880ac9a755fd29b2688956bd959f933f8", // WETH + "0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3", // DAI + "0x55d398326f99059ff775485246999027b3197955", // USDC + "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", // USDT +]; + +pub const WHITELIST_TOKENS: [&str; 11] = [ + "0x2170ed0880ac9a755fd29b2688956bd959f933f8", // WETH + "0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3", // DAI + "0x55d398326f99059ff775485246999027b3197955", // USDC + "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", // USDT + "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", // wBNB + "0x3ee2200efb3400fabb9aacf31297cbdd1d435d47", // ADA + "0xcc42724c6683b7e57334c4e856f4c9965ed682bd", // Matic + "0xf8a0bf9cf54bb92f17374d9e9a321e6a111a51bd", // Chainlink + "0xbf5140a22578168fd562dccf235e5d43a02ce9b1", // UNI + "0xad29abb318791d579433d831ed122afeaf29dcfe", // FTM + "0x26433c8127d9b4e9b71eaa15111df99ea2eeb2f8", // MANA +]; \ No newline at end of file diff --git a/pancake-v2/src/common/helpers.rs b/pancake-v2/src/common/helpers.rs new file mode 100644 index 00000000..e06c8ba5 --- /dev/null +++ b/pancake-v2/src/common/helpers.rs @@ -0,0 +1,99 @@ +use ethabi::ethereum_types::H160; +use substreams::scalar::BigInt; +use substreams_helper::hex::Hexable; + +use crate::pb::erc20::v1::Erc20Token; +use crate::pb::pancake::v2::Pool; +use crate::{abi::ERC20, store_key::StoreKey}; +use substreams::store::{StoreGet, StoreGetBigInt}; + +pub struct TokenContract(H160); + +impl TokenContract { + pub fn new(address: H160) -> Self { + TokenContract(address) + } + + fn get_name(&self) -> String { + ERC20::functions::Name {} + .call(self.0.as_bytes().to_vec()) + .unwrap_or(String::new()) + } + + fn get_symbol(&self) -> String { + ERC20::functions::Symbol {} + .call(self.0.as_bytes().to_vec()) + .unwrap_or(String::new()) + } + + fn get_decimals(&self) -> BigInt { + ERC20::functions::Decimals {} + .call(self.0.as_bytes().to_vec()) + .unwrap_or(BigInt::from(18)) + } + + pub fn as_struct(&self) -> Erc20Token { + Erc20Token { + name: self.get_name(), + symbol: self.get_symbol(), + address: self.0.to_hex(), + decimals: self.get_decimals().try_into().unwrap_or(18), + } + } +} + +impl Pool { + pub fn token0_ref(&self) -> Erc20Token { + self.input_tokens.as_ref().unwrap().items[0].clone() + } + + pub fn token0_address(&self) -> String { + self.token0_ref().address + } + + pub fn token0_decimals(&self) -> u64 { + self.token0_ref().decimals as u64 + } + + pub fn token0_balance(&self, ordinal: u64, balances_store: &StoreGetBigInt) -> BigInt { + balances_store + .get_at( + ordinal, + StoreKey::Token0Balance.get_unique_pool_key(&self.address), + ) + .unwrap_or(BigInt::zero()) + } + + pub fn token1_ref(&self) -> Erc20Token { + self.input_tokens.as_ref().unwrap().items[1].clone() + } + + pub fn token1_address(&self) -> String { + self.token1_ref().address + } + + pub fn token1_decimals(&self) -> u64 { + self.token1_ref().decimals as u64 + } + + pub fn token1_balance(&self, ordinal: u64, balances_store: &StoreGetBigInt) -> BigInt { + balances_store + .get_at( + ordinal, + StoreKey::Token1Balance.get_unique_pool_key(&self.address), + ) + .unwrap_or(BigInt::zero()) + } + + pub fn input_tokens(&self) -> Vec { + vec![self.token0_address(), self.token1_address()] + } + + pub fn output_token_ref(&self) -> Erc20Token { + self.output_token.clone().unwrap() + } + + pub fn output_token_address(&self) -> String { + self.output_token.clone().unwrap().address + } +} diff --git a/pancake-v2/src/common/mod.rs b/pancake-v2/src/common/mod.rs new file mode 100644 index 00000000..3eaf158a --- /dev/null +++ b/pancake-v2/src/common/mod.rs @@ -0,0 +1,8 @@ +#[path = "traits.rs"] +pub(crate) mod traits; + +#[path = "constants.rs"] +pub(crate) mod constants; + +#[path = "helpers.rs"] +pub(crate) mod helpers; diff --git a/pancake-v2/src/common/traits.rs b/pancake-v2/src/common/traits.rs new file mode 100644 index 00000000..a346c2d2 --- /dev/null +++ b/pancake-v2/src/common/traits.rs @@ -0,0 +1,68 @@ +use ethabi::ethereum_types::Address; +use substreams::scalar::{BigDecimal, BigInt}; +use substreams::store::{StoreAdd, StoreAddBigDecimal, StoreAddBigInt}; +use substreams::store::{StoreDelete, StoreGet, StoreGetProto}; +use substreams_helper::common::HasAddresser; +use substreams_helper::hex::Hexable; + +use crate::pb::pancake::v2::Pool; +use crate::store_key::StoreKey; + +pub struct PoolAddresser<'a> { + pub store: &'a StoreGetProto, +} + +impl<'a> PoolAddresser<'a> { + fn has_address(&self, key: Address) -> bool { + let pool = self + .store + .get_last(StoreKey::Pool.get_unique_pool_key(&key.to_hex())); + + pool.is_some() + } +} + +impl<'a> HasAddresser for PoolAddresser<'a> { + fn has_address(&self, key: Address) -> bool { + return self.has_address(key); + } +} + +pub trait StoreAddSnapshot { + fn add_snapshot>(&self, ord: u64, id: i64, k: StoreKey, keys: Vec, value: V); + fn add_protocol_snapshot(&self, ord: u64, id: i64, k: StoreKey, value: V); +} + +impl> StoreAddSnapshot for StoreAddBigDecimal { + fn add_snapshot>(&self, ord: u64, id: i64, k: StoreKey, keys: Vec, value: V) { + let keys: Vec<&str> = keys.iter().map(AsRef::as_ref).collect(); + + self.delete_prefix(ord as i64, &format!("{}:{}", k.unique_id(), id - 1)); + self.add(ord, k.get_unique_snapshot_key(id, keys), value); + } + + fn add_protocol_snapshot(&self, ord: u64, id: i64, k: StoreKey, value: V) { + self.delete_prefix( + ord as i64, + &format!("[Protocol]:{}:{}", k.unique_id(), id - 1), + ); + self.add(ord, k.get_unique_daily_protocol_key(id), value); + } +} + +impl> StoreAddSnapshot for StoreAddBigInt { + fn add_snapshot>(&self, ord: u64, id: i64, k: StoreKey, keys: Vec, value: V) { + let keys: Vec<&str> = keys.iter().map(AsRef::as_ref).collect(); + + self.delete_prefix(ord as i64, &format!("{}:{}", k.unique_id(), id - 1)); + self.add(ord, k.get_unique_snapshot_key(id, keys), value); + } + + fn add_protocol_snapshot(&self, ord: u64, id: i64, k: StoreKey, value: V) { + self.delete_prefix( + ord as i64, + &format!("[Protocol]:{}:{}", k.unique_id(), id - 1), + ); + self.add(ord, k.get_unique_daily_protocol_key(id), value); + } +} diff --git a/pancake-v2/src/lib.rs b/pancake-v2/src/lib.rs new file mode 100644 index 00000000..c3d77663 --- /dev/null +++ b/pancake-v2/src/lib.rs @@ -0,0 +1,8 @@ +mod abi; +mod common; +mod modules; +mod pb; +mod store_key; +mod utils; + +pub use modules::*; diff --git a/pancake-v2/src/modules/1_map_pool_created.rs b/pancake-v2/src/modules/1_map_pool_created.rs new file mode 100644 index 00000000..14be8956 --- /dev/null +++ b/pancake-v2/src/modules/1_map_pool_created.rs @@ -0,0 +1,48 @@ +use std::str::FromStr; + +use ethabi::ethereum_types::Address; +use substreams_ethereum::pb::eth::v2::{self as eth}; +use substreams_helper::event_handler::EventHandler; + +use crate::abi::Factory::events::PairCreated; +use crate::common::constants; +use crate::common::helpers::TokenContract; +use crate::pb::erc20::v1::Erc20Tokens; +use crate::pb::pancake::v2::Pool; +use crate::pb::pancake::v2::Pools; + +#[substreams::handlers::map] +pub fn map_pool_created(block: eth::Block) -> Result { + let mut pools: Vec = vec![]; + + get_pools(&block, &mut pools); + Ok(Pools { pools }) +} + +fn get_pools(block: ð::Block, pools: &mut Vec) { + let mut on_pair_created = |event: PairCreated, _tx: ð::TransactionTrace, _log: ð::Log| { + let pool = TokenContract::new(Address::from_slice(event.pair.as_slice())).as_struct(); + let token0 = TokenContract::new(Address::from_slice(event.token0.as_slice())).as_struct(); + let token1 = TokenContract::new(Address::from_slice(event.token1.as_slice())).as_struct(); + + pools.push(Pool { + name: format! {"{}/{}", token0.symbol, token1.symbol}, + symbol: String::new(), + address: pool.address.clone(), + input_tokens: Some(Erc20Tokens { + items: vec![token0, token1], + }), + output_token: Some(pool), + created_timestamp: block.timestamp_seconds() as i64, + created_block_number: block.number as i64, + }) + }; + + let mut eh = EventHandler::new(&block); + eh.filter_by_address(vec![ + Address::from_str(constants::PANCAKE_V2_FACTORY).unwrap() + ]); + + eh.on::(&mut on_pair_created); + eh.handle_events(); +} diff --git a/pancake-v2/src/modules/2_store_pools.rs b/pancake-v2/src/modules/2_store_pools.rs new file mode 100644 index 00000000..515dac99 --- /dev/null +++ b/pancake-v2/src/modules/2_store_pools.rs @@ -0,0 +1,13 @@ +use substreams::store::StoreNew; +use substreams::store::StoreSetIfNotExists; +use substreams::store::StoreSetIfNotExistsProto; + +use crate::pb::pancake::v2::{Pool, Pools}; +use crate::store_key::StoreKey; + +#[substreams::handlers::store] +pub fn store_pools(pools_created: Pools, store: StoreSetIfNotExistsProto) { + for pool in pools_created.pools { + store.set_if_not_exists(0, StoreKey::Pool.get_unique_pool_key(&pool.address), &pool); + } +} diff --git a/pancake-v2/src/modules/3_store_output_token_supply.rs b/pancake-v2/src/modules/3_store_output_token_supply.rs new file mode 100644 index 00000000..adf19b80 --- /dev/null +++ b/pancake-v2/src/modules/3_store_output_token_supply.rs @@ -0,0 +1,48 @@ +use substreams::store::{StoreAdd, StoreAddBigInt, StoreGet, StoreGetProto, StoreNew}; +use substreams_ethereum::pb::eth::v2::{self as eth}; +use substreams_ethereum::NULL_ADDRESS; + +use substreams_helper::event_handler::EventHandler; +use substreams_helper::hex::Hexable; + +use crate::abi::Pool::events::Transfer; +use crate::common::traits::PoolAddresser; +use crate::{pb::pancake::v2::Pool, store_key::StoreKey}; + +#[substreams::handlers::store] +pub fn store_output_token_supply( + block: eth::Block, + pool_store: StoreGetProto, + output_store: StoreAddBigInt, +) { + let mut on_transfer = |event: Transfer, _tx: ð::TransactionTrace, log: ð::Log| { + let is_burn = event.to == NULL_ADDRESS; + let is_mint = event.from == NULL_ADDRESS; + + if !(is_mint || is_burn) || (is_mint && is_burn) { + return; + } + + let value = event.value; + let pool_address = log.address.to_hex(); + + if is_burn { + output_store.add( + log.ordinal, + StoreKey::OutputTokenBalance.get_unique_pool_key(&pool_address), + value.neg(), + ); + } else { + output_store.add( + log.ordinal, + StoreKey::OutputTokenBalance.get_unique_pool_key(&pool_address), + value, + ); + } + }; + + let mut eh = EventHandler::new(&block); + eh.filter_by_address(PoolAddresser { store: &pool_store }); + eh.on::(&mut on_transfer); + eh.handle_events(); +} diff --git a/pancake-v2/src/modules/4_map_pool_events.rs b/pancake-v2/src/modules/4_map_pool_events.rs new file mode 100644 index 00000000..79dcec37 --- /dev/null +++ b/pancake-v2/src/modules/4_map_pool_events.rs @@ -0,0 +1,175 @@ +use substreams::scalar::BigInt; +use substreams::store::StoreGetProto; +use substreams::store::{DeltaBigInt, Deltas, StoreGet}; +use substreams_ethereum::pb::eth::v2::{self as eth}; +use substreams_helper::event_handler::EventHandler; +use substreams_helper::hex::Hexable; + +use crate::abi::Pool::events::{Burn, Mint, Swap, Sync}; +use crate::common::traits::PoolAddresser; +use crate::pb::pancake::v2::event::Type::{DepositType, SwapType, SyncType, WithdrawType}; +use crate::pb::pancake::v2::{DepositEvent, SwapEvent, SyncEvent, WithdrawEvent}; +use crate::pb::pancake::v2::{Event, Events, Pool}; +use crate::store_key::StoreKey; +use crate::utils; + +#[substreams::handlers::map] +pub fn map_pool_events( + block: eth::Block, + pools_store: StoreGetProto, + balance_deltas: Deltas, +) -> Result { + let mut events = vec![]; + + handle_mint(&block, &pools_store, &balance_deltas, &mut events); + handle_burn(&block, &pools_store, &balance_deltas, &mut events); + handle_sync(&block, &pools_store, &mut events); + handle_swap(&block, &pools_store, &mut events); + + Ok(Events { events }) +} + +fn handle_mint( + block: ð::Block, + store: &StoreGetProto, + balance_deltas: &Deltas, + events: &mut Vec, +) { + let mut on_mint = |event: Mint, tx: ð::TransactionTrace, log: ð::Log| { + let pool_address = log.address.to_hex(); + let output_token_mint_amount = + utils::get_output_token_amount(&balance_deltas, &pool_address); + + events.push(Event { + hash: tx.hash.to_hex(), + log_index: log.index, + log_ordinal: log.ordinal, + to: tx.to.to_hex(), + from: tx.from.to_hex(), + block_number: block.number, + timestamp: block.timestamp_seconds(), + pool: pool_address, + r#type: Some(DepositType(DepositEvent { + input_token_amounts: vec![event.amount0, event.amount1] + .iter() + .map(|x| x.to_string()) + .collect(), + output_token_amount: Some(output_token_mint_amount.to_string()), + })), + }); + }; + + let mut eh = EventHandler::new(&block); + eh.filter_by_address(PoolAddresser { store }); + eh.on::(&mut on_mint); + eh.handle_events(); +} + +fn handle_burn( + block: ð::Block, + store: &StoreGetProto, + balance_deltas: &Deltas, + events: &mut Vec, +) { + let mut on_burn = |event: Burn, tx: ð::TransactionTrace, log: ð::Log| { + let pool_address = log.address.to_hex(); + let output_token_burnt_amount = + utils::get_output_token_amount(&balance_deltas, &pool_address); + + events.push(Event { + hash: tx.hash.to_hex(), + log_index: log.block_index, + log_ordinal: log.ordinal, + to: tx.to.to_hex(), + from: tx.from.to_hex(), + block_number: block.number, + timestamp: block.timestamp_seconds(), + pool: pool_address, + r#type: Some(WithdrawType(WithdrawEvent { + input_token_amounts: vec![event.amount0, event.amount1] + .iter() + .map(|x| x.to_string()) + .collect(), + output_token_amount: Some(output_token_burnt_amount.neg().to_string()), + })), + }); + }; + + let mut eh = EventHandler::new(&block); + eh.filter_by_address(PoolAddresser { store }); + eh.on::(&mut on_burn); + eh.handle_events(); +} + +fn handle_sync(block: ð::Block, store: &StoreGetProto, events: &mut Vec) { + let mut on_sync = |event: Sync, tx: ð::TransactionTrace, log: ð::Log| { + let pool_address = log.address.to_hex(); + + events.push(Event { + hash: tx.hash.to_hex(), + log_index: log.block_index, + log_ordinal: log.ordinal, + to: tx.to.to_hex(), + from: tx.from.to_hex(), + block_number: block.number, + timestamp: block.timestamp_seconds(), + pool: pool_address, + r#type: Some(SyncType(SyncEvent { + reserve0: event.reserve0.to_string(), + reserve1: event.reserve1.to_string(), + })), + }); + }; + + let mut eh = EventHandler::new(&block); + eh.filter_by_address(PoolAddresser { store }); + eh.on::(&mut on_sync); + eh.handle_events(); +} + +fn handle_swap(block: ð::Block, store: &StoreGetProto, events: &mut Vec) { + let mut on_swap = |event: Swap, tx: ð::TransactionTrace, log: ð::Log| { + let pool_address = log.address.to_hex(); + + events.push(Event { + hash: tx.hash.to_hex(), + log_index: log.block_index, + log_ordinal: log.ordinal, + to: tx.to.to_hex(), + from: tx.from.to_hex(), + block_number: block.number, + timestamp: block.timestamp_seconds(), + pool: pool_address.clone(), + r#type: Some(SwapType(get_swap_event(event, &pool_address, &store))), + }); + }; + + let mut eh = EventHandler::new(&block); + eh.filter_by_address(PoolAddresser { store }); + eh.on::(&mut on_swap); + eh.handle_events(); +} + +fn get_swap_event( + event: Swap, + pool_address: &String, + pools_store: &StoreGetProto, +) -> SwapEvent { + let pool = pools_store.must_get_last(StoreKey::Pool.get_unique_pool_key(pool_address)); + + if event.amount0_out.gt(BigInt::zero().as_ref()) { + SwapEvent { + token_in: Some(pool.token1_ref()), + amount_in: (event.amount1_in - event.amount1_out).to_string(), + token_out: Some(pool.token0_ref()), + amount_out: (event.amount0_out - event.amount0_in).to_string(), + } + } else { + SwapEvent { + token_in: Some(pool.token0_ref()), + amount_in: (event.amount0_in - event.amount0_out).to_string(), + token_out: Some(pool.token1_ref()), + amount_out: (event.amount1_out - event.amount1_in).to_string(), + } + } +} diff --git a/pancake-v2/src/modules/5_store_input_token_balances.rs b/pancake-v2/src/modules/5_store_input_token_balances.rs new file mode 100644 index 00000000..14523043 --- /dev/null +++ b/pancake-v2/src/modules/5_store_input_token_balances.rs @@ -0,0 +1,57 @@ +use std::ops::Add; +use std::str::FromStr; + +use substreams::scalar::BigInt; +use substreams::store::StoreNew; +use substreams::store::StoreSet; +use substreams::store::StoreSetBigInt; + +use crate::pb::pancake::v2::event::Type::SyncType; +use crate::pb::pancake::v2::Events; +use crate::store_key::StoreKey; +use crate::utils; + +#[substreams::handlers::store] +pub fn store_input_token_balances(pool_events: Events, output_store: StoreSetBigInt) { + for event in pool_events.events { + match event.r#type.unwrap() { + SyncType(sync_event) => { + let pool_address = event.pool; + + let day_id = utils::get_day_id(event.timestamp as i64); + + let amount0 = BigInt::from_str(sync_event.reserve0.as_str()).unwrap(); + let amount1 = BigInt::from_str(sync_event.reserve1.as_str()).unwrap(); + + output_store.set( + event.log_ordinal, + StoreKey::TotalBalance.get_unique_pool_key(&pool_address), + &amount0.clone().add(amount1.clone()), + ); + output_store.set( + event.log_ordinal, + StoreKey::LatestBlockNumber.unique_id(), + &BigInt::from(event.block_number), + ); + output_store.set( + event.log_ordinal, + StoreKey::LatestTimestamp + .get_unique_snapshot_tracking_key(&pool_address, &day_id.to_string()), + &BigInt::from(event.timestamp as i64), + ); + + output_store.set( + event.log_ordinal, + StoreKey::Token0Balance.get_unique_pool_key(&pool_address), + &amount0, + ); + output_store.set( + event.log_ordinal, + StoreKey::Token1Balance.get_unique_pool_key(&pool_address), + &amount1, + ); + } + _ => {} + } + } +} diff --git a/pancake-v2/src/modules/6_store_native_prices.rs b/pancake-v2/src/modules/6_store_native_prices.rs new file mode 100644 index 00000000..0651f804 --- /dev/null +++ b/pancake-v2/src/modules/6_store_native_prices.rs @@ -0,0 +1,60 @@ +use substreams::store::{DeltaBigInt, Deltas, StoreSet}; +use substreams::store::{StoreGet, StoreGetBigInt, StoreGetProto, StoreNew, StoreSetBigDecimal}; + +use crate::pb::pancake::v2::Pool; +use crate::store_key::StoreKey; + +#[substreams::handlers::store] +pub fn store_native_prices( + pool_store: StoreGetProto, + balances_store: StoreGetBigInt, + balances_deltas: Deltas, + output_store: StoreSetBigDecimal, +) { + for delta in balances_deltas.deltas { + if let Some(pool_address) = StoreKey::TotalBalance.get_pool(&delta.key) { + let ordinal = delta.ordinal; + let pool = pool_store.must_get_last(StoreKey::Pool.get_unique_pool_key(&pool_address)); + + let token0_native_tvl = pool + .token0_balance(ordinal, &balances_store) + .to_decimal(pool.token0_decimals()); + let token1_native_tvl = pool + .token1_balance(ordinal, &balances_store) + .to_decimal(pool.token1_decimals()); + + if token0_native_tvl.is_zero() || token1_native_tvl.is_zero() { + continue; + } + + let token0_price = token1_native_tvl.clone() / token0_native_tvl.clone(); + let token1_price = token0_native_tvl.clone() / token1_native_tvl.clone(); + + output_store.set( + ordinal, + StoreKey::TokenPrice + .get_unique_pair_key(&pool.token0_address(), &pool.token1_address()), + &token0_price, + ); + output_store.set( + ordinal, + StoreKey::TokenBalance + .get_unique_pair_key(&pool.token0_address(), &pool.token1_address()), + &token1_native_tvl, + ); + + output_store.set( + ordinal, + StoreKey::TokenPrice + .get_unique_pair_key(&pool.token1_address(), &pool.token0_address()), + &token1_price, + ); + output_store.set( + ordinal, + StoreKey::TokenBalance + .get_unique_pair_key(&pool.token1_address(), &pool.token0_address()), + &token0_native_tvl, + ); + } + } +} diff --git a/pancake-v2/src/modules/7_map_token_entity.rs b/pancake-v2/src/modules/7_map_token_entity.rs new file mode 100644 index 00000000..69c85de1 --- /dev/null +++ b/pancake-v2/src/modules/7_map_token_entity.rs @@ -0,0 +1,40 @@ +use substreams::scalar::BigDecimal; +use substreams::store::{DeltaBigDecimal, Deltas}; +use substreams_entity_change::pb::entity::{entity_change, EntityChange, EntityChanges}; + +use crate::pb::erc20::v1::Erc20Token; +use crate::pb::pancake::v2::Pools; +use crate::store_key::StoreKey; + +#[substreams::handlers::map] +pub fn map_token_entity( + pools_created: Pools +) -> Result { + let mut entity_changes: Vec = vec![]; + + for pool in pools_created.pools { + for token in [ + pool.token0_ref(), + pool.token1_ref(), + pool.output_token_ref(), + ] { + entity_changes.push(init_token(&token)) + } + } + + Ok(EntityChanges { entity_changes }) +} + +fn init_token(token: &Erc20Token) -> EntityChange { + let mut token_entity_change = + EntityChange::new("Token", &token.address, 0, entity_change::Operation::Create); + + token_entity_change + .change("id", &token.address) + .change("name", &token.name) + .change("symbol", &token.symbol) + .change("decimals", token.decimals as i32); + + token_entity_change +} + diff --git a/pancake-v2/src/modules/8_map_events_entity.rs b/pancake-v2/src/modules/8_map_events_entity.rs new file mode 100644 index 00000000..fc281b02 --- /dev/null +++ b/pancake-v2/src/modules/8_map_events_entity.rs @@ -0,0 +1,171 @@ +use std::ops::Mul; +use std::str::FromStr; + +use substreams::scalar::{BigDecimal, BigInt}; +use substreams::store::{StoreGet, StoreGetBigDecimal, StoreGetProto}; +use substreams_entity_change::pb::entity::{entity_change::Operation, EntityChange, EntityChanges}; + +use crate::utils; +use crate::common::constants; +use crate::store_key::StoreKey; +use crate::pb::erc20::v1::Erc20Token; +use crate::pb::pancake::v2::event::Type::{DepositType, SwapType, WithdrawType}; +use crate::pb::pancake::v2::{DepositEvent, Event, Events, Pool, SwapEvent, WithdrawEvent}; + +#[substreams::handlers::map] +pub fn map_events_entity( + pool_events_map: Events, + pool_store: StoreGetProto, +) -> Result { + let mut entity_changes: Vec = vec![]; + + for event in pool_events_map.events { + let ordinal = event.log_ordinal as u64; + + let pool = + pool_store.must_get_last(StoreKey::Pool.get_unique_pool_key(&event.clone().pool)); + + match event.clone().r#type.unwrap() { + DepositType(deposit) => { + entity_changes.push(create_deposit_transaction(ordinal, &pool, &event, &deposit)) + } + WithdrawType(withdraw) => entity_changes.push(create_withdraw_transaction( + ordinal, &pool, &event, &withdraw, + )), + SwapType(swap) => entity_changes.push(create_swap_transaction(ordinal, &event, &swap)), + _ => {} + } + } + + Ok(EntityChanges { entity_changes }) +} + +fn get_event_id(event: &Event) -> String { + [event.hash.clone(), event.log_index.to_string()].join("-") +} + +fn calculate_event_amount_usd( + ordinal: u64, + input_tokens: Vec, + amounts: Vec, + store: &StoreGetBigDecimal, +) -> BigDecimal { + let mut amount_usd = BigDecimal::zero(); + + for (idx, token) in input_tokens.iter().enumerate() { + let token_amount = BigInt::try_from(amounts[idx].clone()).unwrap(); + let token_price = utils::get_token_price(ordinal, store, &token.address); + + amount_usd = amount_usd + token_price.mul(token_amount.to_decimal(token.decimals)) + } + + amount_usd +} + +fn create_deposit_transaction( + ordinal: u64, + pool: &Pool, + event: &Event, + deposit: &DepositEvent, +) -> EntityChange { + let id = get_event_id(event); + + let mut deposit_entity_change = + EntityChange::new("Deposit", id.as_str(), ordinal, Operation::Create); + + let input_tokens = pool.input_tokens.as_ref().unwrap().items.clone(); + let input_token_amounts = deposit.input_token_amounts.clone(); + let output_token_amount = + BigInt::try_from(deposit.output_token_amount.as_ref().unwrap().clone()).unwrap(); + + deposit_entity_change + .change("id", id) + .change("hash", event.hash.clone()) + .change("logIndex", event.log_index as i32) + .change("protocol", constants::PANCAKE_V2_FACTORY.to_string()) + .change("to", event.to.clone()) + .change("from", event.from.clone()) + .change("blockNumber", BigInt::from(event.block_number)) + .change("timestamp", BigInt::from(event.timestamp)) + .change("inputTokens", pool.input_tokens()) + .change("outputToken", pool.output_token_address()) + .change("inputTokenAmounts", input_token_amounts) + .change("outputTokenAmount", output_token_amount) + .change("amountUSD", BigDecimal::zero()) + .change("pool", pool.address.clone()); + + deposit_entity_change +} + +fn create_withdraw_transaction( + ordinal: u64, + pool: &Pool, + event: &Event, + withdraw: &WithdrawEvent, +) -> EntityChange { + let id = get_event_id(event); + + let mut withdraw_entity_change: EntityChange = + EntityChange::new("Withdraw", id.as_str(), ordinal, Operation::Create); + + let input_tokens = pool.input_tokens.as_ref().unwrap().items.clone(); + let input_token_amounts = withdraw.input_token_amounts.clone(); + let output_token_amount = + BigInt::try_from(withdraw.output_token_amount.as_ref().unwrap().clone()).unwrap(); + + withdraw_entity_change + .change("id", id) + .change("hash", event.hash.clone()) + .change("logIndex", event.log_index as i32) + .change("protocol", constants::PANCAKE_V2_FACTORY.to_string()) + .change("to", event.to.clone()) + .change("from", event.from.clone()) + .change("blockNumber", BigInt::from(event.block_number)) + .change("timestamp", BigInt::from(event.timestamp)) + .change("inputTokens", pool.input_tokens()) + .change("outputToken", pool.output_token_address()) + .change("inputTokenAmounts", input_token_amounts) + .change("outputTokenAmount", output_token_amount) + .change("amountUSD", BigDecimal::zero()) + .change("pool", pool.address.clone()); + + withdraw_entity_change +} + +fn create_swap_transaction(ordinal: u64, event: &Event, swap: &SwapEvent) -> EntityChange { + let id = get_event_id(event); + + let mut swap_entity_change: EntityChange = + EntityChange::new("Swap", id.as_str(), ordinal, Operation::Create); + + let token_in = swap.token_in.clone().unwrap(); + let token_out = swap.token_out.clone().unwrap(); + + let token_in_price = BigDecimal::zero(); + let token_out_price = BigDecimal::zero(); + + let amount_in = BigInt::from_str(swap.amount_in.as_str()).unwrap(); + let amount_out = BigInt::from_str(swap.amount_out.as_str()).unwrap(); + + let amount_in_usd = amount_in.to_decimal(token_in.decimals) * token_in_price; + let amount_out_usd = amount_out.to_decimal(token_out.decimals) * token_out_price; + + swap_entity_change + .change("id", id) + .change("hash", event.hash.clone()) + .change("logIndex", event.log_index as i32) + .change("protocol", constants::PANCAKE_V2_FACTORY.to_string()) + .change("to", event.to.clone()) + .change("from", event.from.clone()) + .change("blockNumber", BigInt::from(event.block_number)) + .change("timestamp", BigInt::from(event.timestamp)) + .change("tokenIn", token_in.address) + .change("amountIn", amount_in) + .change("amountInUSD", amount_in_usd) + .change("tokenOut", token_out.address) + .change("amountOut", amount_out) + .change("amountOutUSD", amount_out_usd) + .change("pool", event.pool.clone()); + + swap_entity_change +} diff --git a/pancake-v2/src/modules/9_graph_out.rs b/pancake-v2/src/modules/9_graph_out.rs new file mode 100644 index 00000000..0d5132d5 --- /dev/null +++ b/pancake-v2/src/modules/9_graph_out.rs @@ -0,0 +1,14 @@ +use substreams_entity_change::pb::entity::{EntityChange, EntityChanges}; + +#[substreams::handlers::map] +pub fn graph_out( + token_map: EntityChanges, + events_map: EntityChanges, +) -> Result { + let mut entity_changes: Vec = vec![]; + + entity_changes.extend(token_map.entity_changes); + entity_changes.extend(events_map.entity_changes); + + Ok(EntityChanges { entity_changes }) +} diff --git a/pancake-v2/src/modules/mod.rs b/pancake-v2/src/modules/mod.rs new file mode 100644 index 00000000..f6d6c64a --- /dev/null +++ b/pancake-v2/src/modules/mod.rs @@ -0,0 +1,36 @@ +#[path = "1_map_pool_created.rs"] +mod map_pool_created; + +#[path = "2_store_pools.rs"] +mod store_pools; + +#[path = "3_store_output_token_supply.rs"] +mod store_output_token_supply; + +#[path = "4_map_pool_events.rs"] +mod map_pool_events; + +#[path = "5_store_input_token_balances.rs"] +mod store_input_token_balances; + +#[path = "6_store_native_prices.rs"] +mod store_native_prices; + +#[path = "7_map_token_entity.rs"] +mod map_token_entity; + +#[path = "8_map_events_entity.rs"] +mod map_events_entity; + +#[path = "9_graph_out.rs"] +mod graph_out; + +pub use map_pool_created::map_pool_created; +pub use store_pools::store_pools; +pub use store_output_token_supply::store_output_token_supply; +pub use map_pool_events::map_pool_events; +pub use store_input_token_balances::store_input_token_balances; +pub use store_native_prices::store_native_prices; +pub use map_token_entity::map_token_entity; +pub use map_events_entity::map_events_entity; +pub use graph_out::graph_out; diff --git a/pancake-v2/src/pb.rs b/pancake-v2/src/pb.rs new file mode 100644 index 00000000..6bdd351c --- /dev/null +++ b/pancake-v2/src/pb.rs @@ -0,0 +1,39 @@ +#[rustfmt::skip] +#[path = "../target/pb/messari.common.v1.rs"] +pub(in crate::pb) mod common_v1; + +pub mod common { + pub mod v1 { + pub use super::super::common_v1::*; + } +} + +#[rustfmt::skip] +#[path = "../target/pb/substreams.entity.v1.rs"] +pub(in crate::pb) mod entity_v1; + +pub mod entity { + pub mod v1 { + pub use super::super::entity_v1::*; + } +} + +#[rustfmt::skip] +#[path = "../target/pb/messari.erc20.v1.rs"] +pub(in crate::pb) mod erc20_v1; + +pub mod erc20 { + pub mod v1 { + pub use super::super::erc20_v1::*; + } +} + +#[rustfmt::skip] +#[path = "../target/pb/messari.pancake.v2.rs"] +pub(in crate::pb) mod pancake_v2; + +pub mod pancake { + pub mod v2 { + pub use super::super::pancake_v2::*; + } +} diff --git a/pancake-v2/src/store_key.rs b/pancake-v2/src/store_key.rs new file mode 100644 index 00000000..727ecd0b --- /dev/null +++ b/pancake-v2/src/store_key.rs @@ -0,0 +1,108 @@ +#[derive(Clone)] +pub enum StoreKey { + Pool, + TotalBalance, + LatestTimestamp, + LatestBlockNumber, + TokenBalance, + Token0Balance, + Token1Balance, + OutputTokenBalance, + TokenPrice, + TotalValueLockedUSD, + Volume, + DailySupplySideRevenueUSD, + HourlySupplySideRevenueUSD, + CumulativeSupplySideRevenueUSD, + DailyProtocolSideRevenueUSD, + HourlyProtocolSideRevenueUSD, + CumulativeProtocolSideRevenueUSD, + DailyTotalRevenueUSD, + HourlyTotalRevenueUSD, + CumulativeTotalRevenueUSD, + DailyVolumeUSD, + HourlyVolumeUSD, + CumulativeVolumeUSD, + VolumeByTokenUSD, + DailyVolumeByTokenUSD, + HourlyVolumeByTokenUSD, + DailyVolumeByTokenAmount, + HourlyVolumeByTokenAmount, +} + +impl StoreKey { + pub fn get_unique_pool_key(&self, key: &str) -> String { + format!("{}:{}", self.unique_id(), key) + } + + pub fn get_unique_pair_key(&self, key1: &str, key2: &str) -> String { + format!("{}:{}:{}", self.unique_id(), key1, key2) + } + + pub fn get_unique_protocol_key(&self) -> String { + format!("[Protocol]:{}", self.unique_id()) + } + + pub fn get_unique_snapshot_key(&self, id: i64, keys: Vec<&str>) -> String { + format!("{}:{}:{}", self.unique_id(), id, keys.join(":")) + } + + pub fn get_unique_daily_protocol_key(&self, day_id: i64) -> String { + format!("[Protocol]:{}:{}", self.unique_id(), day_id) + } + + pub fn get_unique_snapshot_tracking_key(&self, key1: &str, key2: &str) -> String { + format!("{}:{}:{}", self.unique_id(), key1, key2) + } + + pub fn get_pool(&self, key: &str) -> Option { + let chunks: Vec<&str> = key.split(":").collect(); + + if chunks[0] != self.unique_id() { + return None; + } + return Some(chunks[1].to_string()); + } + + pub fn get_pool_and_token(&self, key: &str) -> Option<(String, String)> { + let chunks: Vec<&str> = key.split(":").collect(); + + if chunks[0] != self.unique_id() { + return None; + } + return Some((chunks[1].to_string(), chunks[2].to_string())); + } + + pub fn unique_id(&self) -> String { + match self { + StoreKey::Pool => "Pool".to_string(), + StoreKey::TotalBalance => "TotalBalance".to_string(), + StoreKey::LatestTimestamp => "LatestTimestamp".to_string(), + StoreKey::LatestBlockNumber => "LatestBlockNumber".to_string(), + StoreKey::TokenBalance => "TokenBalance".to_string(), + StoreKey::Token0Balance => "Token0Balance".to_string(), + StoreKey::Token1Balance => "Token1Balance".to_string(), + StoreKey::OutputTokenBalance => "OutputTokenBalance".to_string(), + StoreKey::TotalValueLockedUSD => "TotalValueLockedUSD".to_string(), + StoreKey::TokenPrice => "TokenPrice".to_string(), + StoreKey::Volume => "Volume".to_string(), + StoreKey::DailySupplySideRevenueUSD => "d:SupplySideRevenueUSD".to_string(), + StoreKey::HourlySupplySideRevenueUSD => "h:SupplySideRevenueUSD".to_string(), + StoreKey::CumulativeSupplySideRevenueUSD => "c:SupplySideRevenueUSD".to_string(), + StoreKey::DailyProtocolSideRevenueUSD => "d:ProtocolSideRevenueUSD".to_string(), + StoreKey::HourlyProtocolSideRevenueUSD => "h:ProtocolSideRevenueUSD".to_string(), + StoreKey::CumulativeProtocolSideRevenueUSD => "c:ProtocolSideRevenueUSD".to_string(), + StoreKey::DailyTotalRevenueUSD => "d:TotalRevenueUSD".to_string(), + StoreKey::HourlyTotalRevenueUSD => "h:TotalRevenueUSD".to_string(), + StoreKey::CumulativeTotalRevenueUSD => "c:TotalRevenueUSD".to_string(), + StoreKey::DailyVolumeUSD => "d:VolumeUSD".to_string(), + StoreKey::HourlyVolumeUSD => "h:VolumeUSD".to_string(), + StoreKey::CumulativeVolumeUSD => "c:VolumeUSD".to_string(), + StoreKey::VolumeByTokenUSD => "VolumeByTokenUSD".to_string(), + StoreKey::DailyVolumeByTokenUSD => "d:VolumeByTokenUSD".to_string(), + StoreKey::HourlyVolumeByTokenUSD => "h:VolumeByTokenUSD".to_string(), + StoreKey::DailyVolumeByTokenAmount => "d:VolumeByTokenAmount".to_string(), + StoreKey::HourlyVolumeByTokenAmount => "h:VolumeByTokenAmount".to_string(), + } + } +} diff --git a/pancake-v2/src/utils.rs b/pancake-v2/src/utils.rs new file mode 100644 index 00000000..b5f2ba61 --- /dev/null +++ b/pancake-v2/src/utils.rs @@ -0,0 +1,58 @@ +use std::ops::Sub; + +use crate::common::constants; +use crate::store_key::StoreKey; +use substreams::scalar::{BigDecimal, BigInt}; +use substreams::store::{DeltaBigDecimal, DeltaBigInt, Deltas}; +use substreams::store::{StoreGet, StoreGetBigDecimal}; + +pub fn get_day_id(timestamp: i64) -> i64 { + const SECONDS_IN_DAY: i64 = 86400_i64; + timestamp / SECONDS_IN_DAY +} + +pub fn get_hour_id(timestamp: i64) -> i64 { + const SECONDS_IN_HOUR: i64 = 3600_i64; + timestamp / SECONDS_IN_HOUR +} + +pub fn is_pricing_asset(address: &String) -> bool { + constants::STABLE_COINS.contains(&address.as_str()) + || constants::WHITELIST_TOKENS.contains(&address.as_str()) +} + +pub fn delta_value(delta: &DeltaBigDecimal) -> BigDecimal { + let old_value = delta.old_value.clone(); + let new_value = delta.new_value.clone(); + + return new_value.sub(old_value); +} + +pub fn calculate_revenue(volume: BigDecimal) -> (BigDecimal, BigDecimal) { + let supply_side_revenue = + volume.clone() * BigDecimal::from(25_i32) / BigDecimal::from(10000_i32); + let protocol_side_revenue = volume * BigDecimal::from(5_i32) / BigDecimal::from(10000_i32); + + (supply_side_revenue, protocol_side_revenue) +} + +pub fn get_token_price(ordinal: u64, store: &StoreGetBigDecimal, address: &String) -> BigDecimal { + store + .get_at(ordinal, StoreKey::TokenPrice.get_unique_pool_key(address)) + .unwrap_or(BigDecimal::zero()) +} + +pub fn get_output_token_amount( + balance_deltas: &Deltas, + pool_address: &String, +) -> BigInt { + let mut balance_diff = BigInt::zero(); + + for delta in balance_deltas.deltas.iter() { + if delta.key == StoreKey::OutputTokenBalance.get_unique_pool_key(pool_address) { + balance_diff = delta.new_value.clone() - delta.old_value.clone(); + } + } + + balance_diff +} diff --git a/pancake-v2/subgraph.yaml b/pancake-v2/subgraph.yaml new file mode 100644 index 00000000..8f685adb --- /dev/null +++ b/pancake-v2/subgraph.yaml @@ -0,0 +1,17 @@ +specVersion: 0.0.4 +description: Pancake v2 powered by substreams +repository: https://github.com/messari/substreams +schema: + file: ./schema.graphql + +dataSources: + - kind: substreams + name: pancake_v2 + network: bsc + source: + package: + moduleName: graph_out + file: substreams-pancake-v2-v0.1.0.spkg + mapping: + kind: substreams/graph-entities + apiVersion: 0.0.5 diff --git a/pancake-v2/substreams.yaml b/pancake-v2/substreams.yaml new file mode 100644 index 00000000..62ff978d --- /dev/null +++ b/pancake-v2/substreams.yaml @@ -0,0 +1,100 @@ +specVersion: v0.1.0 +package: + name: substreams_pancake_v2 + version: v0.1.0 + +imports: + eth: https://github.com/streamingfast/sf-ethereum/releases/download/v0.10.2/ethereum-v0.10.4.spkg + entities_change: https://github.com/streamingfast/substreams-entity-change/releases/download/v0.2.0/substreams-entity-change-v0.2.0.spkg + +binaries: + default: + type: wasm/rust-v1 + file: "../target/wasm32-unknown-unknown/release/substreams_pancake_v2.wasm" + +protobuf: + files: + - common.proto + - erc20.proto + - pancake.proto + importPaths: + - ./proto/v1 + - ../common/proto + +modules: + - name: map_pool_created + kind: map + initialBlock: 6809737 + inputs: + - source: sf.ethereum.type.v2.Block + output: + type: proto:messari.pancake.v2.Pools + + - name: store_pools + kind: store + updatePolicy: set_if_not_exists + valueType: proto:messari.pancake.v2.Pool + inputs: + - map: map_pool_created + + - name: store_output_token_supply + kind: store + updatePolicy: add + valueType: bigint + inputs: + - source: sf.ethereum.type.v2.Block + - store: store_pools + + - name: map_pool_events + kind: map + initialBlock: 6809737 + inputs: + - source: sf.ethereum.type.v2.Block + - store: store_pools + - store: store_output_token_supply + mode: deltas + output: + type: proto:messari.pancake.v2.Events + + - name: store_input_token_balances + kind: store + updatePolicy: set + valueType: bigint + inputs: + - map: map_pool_events + + - name: store_native_prices + kind: store + updatePolicy: set + valueType: bigdecimal + inputs: + - store: store_pools + - store: store_input_token_balances + - store: store_input_token_balances + mode: deltas + + - name: map_token_entity + kind: map + initialBlock: 6809737 + inputs: + - map: map_pool_created + output: + type: proto:substreams.entity.v1.EntityChanges + + - name: map_events_entity + kind: map + initialBlock: 6809737 + inputs: + - map: map_pool_events + - store: store_pools + output: + type: proto:substreams.entity.v1.EntityChanges + + - name: graph_out + kind: map + initialBlock: 6809737 + inputs: + - map: map_token_entity + - map: map_events_entity + output: + type: proto:substreams.entity.v1.EntityChanges \ No newline at end of file