diff --git a/abis/public_allocator_abi.json b/abis/public_allocator_abi.json new file mode 100644 index 0000000..8bc725d --- /dev/null +++ b/abis/public_allocator_abi.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"morpho","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AlreadySet","type":"error"},{"inputs":[],"name":"DepositMarketInWithdrawals","type":"error"},{"inputs":[],"name":"EmptyWithdrawals","type":"error"},{"inputs":[],"name":"InconsistentWithdrawals","type":"error"},{"inputs":[],"name":"IncorrectFee","type":"error"},{"inputs":[{"internalType":"Id","name":"id","type":"bytes32"}],"name":"MarketNotEnabled","type":"error"},{"inputs":[{"internalType":"Id","name":"id","type":"bytes32"}],"name":"MaxInflowExceeded","type":"error"},{"inputs":[{"internalType":"Id","name":"id","type":"bytes32"}],"name":"MaxOutflowExceeded","type":"error"},{"inputs":[],"name":"MaxSettableFlowCapExceeded","type":"error"},{"inputs":[],"name":"NotAdminNorVaultOwner","type":"error"},{"inputs":[{"internalType":"Id","name":"id","type":"bytes32"}],"name":"NotEnoughSupply","type":"error"},{"inputs":[{"internalType":"Id","name":"id","type":"bytes32"}],"name":"WithdrawZero","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"indexed":true,"internalType":"Id","name":"supplyMarketId","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"suppliedAssets","type":"uint256"}],"name":"PublicReallocateTo","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"indexed":true,"internalType":"Id","name":"id","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"withdrawnAssets","type":"uint256"}],"name":"PublicWithdrawal","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"indexed":false,"internalType":"address","name":"admin","type":"address"}],"name":"SetAdmin","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"}],"name":"SetFee","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"components":[{"internalType":"Id","name":"id","type":"bytes32"},{"components":[{"internalType":"uint128","name":"maxIn","type":"uint128"},{"internalType":"uint128","name":"maxOut","type":"uint128"}],"internalType":"struct FlowCaps","name":"caps","type":"tuple"}],"indexed":false,"internalType":"struct FlowCapsConfig[]","name":"config","type":"tuple[]"}],"name":"SetFlowCaps","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"vault","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"feeRecipient","type":"address"}],"name":"TransferFee","type":"event"},{"inputs":[],"name":"MORPHO","outputs":[{"internalType":"contract IMorpho","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"accruedFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"Id","name":"","type":"bytes32"}],"name":"flowCaps","outputs":[{"internalType":"uint128","name":"maxIn","type":"uint128"},{"internalType":"uint128","name":"maxOut","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"vault","type":"address"},{"components":[{"components":[{"internalType":"address","name":"loanToken","type":"address"},{"internalType":"address","name":"collateralToken","type":"address"},{"internalType":"address","name":"oracle","type":"address"},{"internalType":"address","name":"irm","type":"address"},{"internalType":"uint256","name":"lltv","type":"uint256"}],"internalType":"struct MarketParams","name":"marketParams","type":"tuple"},{"internalType":"uint128","name":"amount","type":"uint128"}],"internalType":"struct Withdrawal[]","name":"withdrawals","type":"tuple[]"},{"components":[{"internalType":"address","name":"loanToken","type":"address"},{"internalType":"address","name":"collateralToken","type":"address"},{"internalType":"address","name":"oracle","type":"address"},{"internalType":"address","name":"irm","type":"address"},{"internalType":"uint256","name":"lltv","type":"uint256"}],"internalType":"struct MarketParams","name":"supplyMarketParams","type":"tuple"}],"name":"reallocateTo","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"vault","type":"address"},{"internalType":"address","name":"newAdmin","type":"address"}],"name":"setAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"vault","type":"address"},{"internalType":"uint256","name":"newFee","type":"uint256"}],"name":"setFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"vault","type":"address"},{"components":[{"internalType":"Id","name":"id","type":"bytes32"},{"components":[{"internalType":"uint128","name":"maxIn","type":"uint128"},{"internalType":"uint128","name":"maxOut","type":"uint128"}],"internalType":"struct FlowCaps","name":"caps","type":"tuple"}],"internalType":"struct FlowCapsConfig[]","name":"config","type":"tuple[]"}],"name":"setFlowCaps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"vault","type":"address"},{"internalType":"address payable","name":"feeRecipient","type":"address"}],"name":"transferFee","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/blueflow.ipynb b/blueflow.ipynb new file mode 100644 index 0000000..c0ef6eb --- /dev/null +++ b/blueflow.ipynb @@ -0,0 +1,376 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "import json\n", + "import requests \n", + "import pandas as pd \n", + "from web3 import Web3\n", + "from pyvis.network import Network\n", + "import pyvis.network as net\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "def number_to_readable(num):\n", + " abs_num = abs(num)\n", + " if abs_num < 1000:\n", + " return str(num)\n", + " elif abs_num < 1000000:\n", + " return f\"{num/1000:.1f}k\".replace('.0', '')\n", + " elif abs_num < 1000000000:\n", + " return f\"{num/1000000:.1f}M\".replace('.0', '')\n", + " elif abs_num < 1000000000000:\n", + " return f\"{num/1000000000:.1f}B\".replace('.0', '')\n", + " else:\n", + " return \"∞\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Markets" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "market_query = \"\"\"\n", + "query{\n", + " markets(first: 1000) {\n", + " items {\n", + " uniqueKey\n", + " loanAsset {\n", + " symbol\n", + " decimals\n", + " priceUsd\n", + " }\n", + " state{\n", + " borrowAssetsUsd\n", + " supplyAssetsUsd\n", + " }\n", + " collateralAsset{\n", + " symbol\n", + " }\n", + " lltv\n", + " morphoBlue{chain{network}}\n", + " }\n", + " }\n", + "}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "blue_api_url = \"https://blue-api.morpho.org/graphql\"\n", + "\n", + "params = {\n", + " \"query\": market_query\n", + "}\n", + "\n", + "response = requests.post(blue_api_url, json=params)\n", + "market_raw_data = response.json()['data']['markets']['items']\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "market_data = []\n", + "\n", + "for market in market_raw_data:\n", + " loan_asset = market['loanAsset']['symbol']\n", + " if market['collateralAsset'] is not None:\n", + " collateral_asset = market['collateralAsset']['symbol']\n", + " else:\n", + " collateral_asset = \"Idle\"\n", + " lltv = int(market['lltv']) / 1e16\n", + " chain = market['morphoBlue']['chain']['network']\n", + " loan_decimals = market['loanAsset']['decimals']\n", + " loan_price_usd = market['loanAsset']['priceUsd']\n", + " supplyAssetsUsd = market['state']['supplyAssetsUsd']\n", + " market_name = f\"{collateral_asset}/{loan_asset} ({lltv}%)\"\n", + " market_id = market['uniqueKey']\n", + " market_data.append([market_name, chain, market_id, loan_decimals, loan_price_usd, supplyAssetsUsd])\n", + "\n", + "market_data = pd.DataFrame(market_data, columns=['market_name', 'market_chain', 'market_id' , 'loan_decimals', 'loan_price_usd', 'supply_assets_usd'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MetaMorpho Vaults" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "vault_query = \"\"\"\n", + "query{\n", + " vaults(first: 1000) {\n", + " items {\n", + " address\n", + " name\n", + " id\n", + " chain {\n", + " network\n", + " }\n", + " state {\n", + " totalAssetsUsd\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "params = {\n", + " \"query\": vault_query\n", + "}\n", + "\n", + "response = requests.post(blue_api_url, json=params)\n", + "response_code = response.status_code\n", + "vault_raw_data = response.json()['data']['vaults']['items']\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "vault_data = []\n", + "\n", + "for vault in vault_raw_data:\n", + " vault_name = vault['name']\n", + " chain = vault['chain']['network']\n", + " address = vault['address']\n", + " vault_id = vault['id']\n", + " total_assets_usd = vault['state']['totalAssetsUsd']\n", + " vault_data.append([vault_name, chain, address, total_assets_usd])\n", + "\n", + "vault_data = pd.DataFrame(vault_data, columns=['vault_name', 'vault_chain', 'vault_address', 'total_assets_usd'])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Flows" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# os.environ['RPC_URL'] = ''\n", + "w3 = Web3(Web3.HTTPProvider(os.environ['RPC_URL']))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "public_allocator_address = \"0xfd32fA2ca22c76dD6E550706Ad913FC6CE91c75D\"\n", + "\n", + "with open('abis/public_allocator_abi.json') as f:\n", + " public_allocator_abi = json.load(f)\n", + "\n", + "public_allocator = w3.eth.contract(address=public_allocator_address, abi=public_allocator_abi)\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "set_flow_caps_events = public_allocator.events.SetFlowCaps.create_filter(fromBlock=0).get_all_entries()\n", + "\n", + "flows = []\n", + "for event in set_flow_caps_events:\n", + " vault = event['args']['vault']\n", + " block_number = event['blockNumber']\n", + " for flow_cap in event['args']['config']:\n", + " market_id, max_in, max_out = '0x' + flow_cap['id'].hex(), flow_cap['caps']['maxIn'], flow_cap['caps']['maxOut']\n", + " flows.append((vault, block_number, market_id, max_in, max_out))\n", + "\n", + "flow_df = pd.DataFrame(flows, columns=['vault_address', 'block_number', 'market_id', 'max_in', 'max_out'])\n", + "flow_df['market_id'] = flow_df['market_id'].apply(lambda x: x.lower()).astype(str)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "flow_df = pd.merge(flow_df, market_data, on='market_id', how='left')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "flow_df = pd.merge(flow_df, vault_data, on='vault_address', how='left')\n", + "flow_df['max_in'] = flow_df['max_in'].astype(float) / 10**flow_df['loan_decimals']\n", + "flow_df['max_out'] = flow_df['max_out'].astype(float) / 10**flow_df['loan_decimals']\n", + "flow_df['max_in_usd'] = flow_df['max_in'] * flow_df['loan_price_usd']\n", + "flow_df['max_out_usd'] = flow_df['max_out'] * flow_df['loan_price_usd']\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create a directed graph" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: When cdn_resources is 'local' jupyter notebook has issues displaying graphics on chrome/safari. Use cdn_resources='in_line' or cdn_resources='remote' if you have issues viewing graphics in a notebook.\n", + "blueflow.html\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "# Create a pyvis Network object\n", + "net = Network(notebook=True, directed=True)\n", + "\n", + "scale = 1e6\n", + "# Add nodes for unique market_ids and vault_ids\n", + "market_ids = flow_df['market_name'].unique()\n", + "vault_ids = flow_df['vault_name'].unique()\n", + "\n", + "for market_id in market_ids:\n", + " value = market_data[market_data['market_name'] == market_id]['supply_assets_usd'].values[0]\n", + " net.add_node(str(market_id), label=f\"Market {market_id}\", color='#ff8000', size=value/scale, level=0)\n", + "\n", + "for vault_id in vault_ids:\n", + " value = vault_data[vault_data['vault_name'] == vault_id]['total_assets_usd'].values[0]\n", + " net.add_node(str(vault_id), label=f\"Vault {vault_id}\", color='#0000FF', size=value/scale, level=1)\n", + "\n", + "# Add edges\n", + "MAX_VALUE = 1e8\n", + "\n", + "for _, row in flow_df.iterrows():\n", + " if row['max_in_usd'] > 0:\n", + " \n", + " net.add_edge(str(row['market_name']), str(row['vault_name']), \n", + " title=f\"Max In: ${number_to_readable(row['max_in_usd'])}\",\n", + " value=min(row['max_in_usd'], MAX_VALUE), color=\"#ff0000\")\n", + " if row['max_out_usd'] > 0:\n", + " net.add_edge(str(row['vault_name']), str(row['market_name']), \n", + " title=f\"Max Out: ${number_to_readable(row['max_out_usd'])}\",\n", + " value=min(row['max_out_usd'], MAX_VALUE), color=\"#00ff00\")\n", + " \n", + "\n", + "# Save and display the graph\n", + "net.show('blueflow.html')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}