From 85a7b5a8c849eccac895d100a689cd2d17e4c4fe Mon Sep 17 00:00:00 2001 From: crebsy <121096251+crebsy@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:54:03 +0200 Subject: [PATCH] yETH exporter, APY calc and new dash (#637) * wip: first apy calc version for yeth * fix: EOW is on a Wednesday 23:59:59 * feat: adding YETHLST helper class. * feat: add net_apy, gross_apr and type labels * fix: use supply and price more consistently. * feat: move yeth code into separate file * feat: added boost * fix: typos * fix: use yETH as the type for the apy subhash * chore: dry some code * feat: get APY for a given block * feat: added historical exporter code for yETH and created first dashboard. * fix: dashboard queries and labels * fix: reverse boost * feat: adding daily swap volumes * feat: add day_ago sample point. * feat: yeth is a new metric * feat: add daily swaps via events * feat: move yeth into separate exporter * fix: add yeth recipe * chore: cleanup * fix: create a nested APY object for yeth * fix: load constants only for Mainnet * fix: revert changes to s3 script * fix: typo in ENV * feat: add recipe for new yeth apy script * feat: allow to pass in a custom resolution per exporter script * fix: missing params and imports * feat: adding separate yeth APY script * fix: replace env * fix: filepaths * fix: whitespace * fix: disable grafana error popup for annotations * feat: showing virtual_balances and tvl denominated in ETH instead of USD. Added weights and target diff chart. * feat: added dropdown for LST selection and some formatting * feat: move LST data into separate dash * Update yETH.json * fix: sanitize name for grafana dashboards * update: yeth lst * fix: yeth dashboard * fix: yeth dashboard2 --------- Co-authored-by: 0xBasic <0xBasic@yearn.finance> --- Makefile | 13 + .../dashboards/yearn/Overview.json | 2 +- .../provisioning/dashboards/yearn/yETH.json | 2334 +++++++++++++++++ scripts/exporters/yeth.py | 62 + scripts/yeth.py | 214 ++ services/dashboard/docker-compose.yml | 2 +- yearn/apy/common.py | 4 +- yearn/helpers/exporter.py | 12 +- yearn/helpers/snapshots.py | 6 +- yearn/yearn.py | 2 + yearn/yeth.py | 275 ++ 11 files changed, 2916 insertions(+), 10 deletions(-) create mode 100644 grafana/provisioning/dashboards/yearn/yETH.json create mode 100644 scripts/exporters/yeth.py create mode 100644 scripts/yeth.py create mode 100644 yearn/yeth.py diff --git a/Makefile b/Makefile index de4820324..86301a59a 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,9 @@ up: if [ "$(commands)" == "exporters/veyfi" ] || [ "$(commands)" == $(exporter_scripts) ] || [ "$(commands)" == "" ]; then make single-network network=ethereum commands="exporters/veyfi" fi + if [ "$(commands)" == "exporters/yeth" ] || [ "$(commands)" == $(exporter_scripts) ] || [ "$(commands)" == "" ]; then + make single-network network=ethereum commands="exporters/yeth" + fi fi # cleanup containers which are temporarily unused or too buggy, ugly workaround until there is a better way to control this: @@ -267,6 +270,12 @@ curve-apy-previews: curve-apy-previews-monitoring: make up commands="curve_apy_previews with_monitoring" network=eth +apy-yeth-monitoring: + make up commands="yeth with_monitoring" network=eth + +apy-yeth: + make up commands="yeth" network=eth filter=yeth + # revenue scripts revenues: make up network=eth commands=revenues with_logs=false @@ -291,6 +300,10 @@ partners-summary-ftm: veyfi: make up network=ethereum commands="exporters/veyfi" logs +# yeth +yeth: + make up network=ethereum commands="exporters/yeth" logs + # utils fetch-memray: mkdir reports/memray -p diff --git a/grafana/provisioning/dashboards/yearn/Overview.json b/grafana/provisioning/dashboards/yearn/Overview.json index 404662035..b5793c997 100644 --- a/grafana/provisioning/dashboards/yearn/Overview.json +++ b/grafana/provisioning/dashboards/yearn/Overview.json @@ -56,7 +56,7 @@ "type": "datasource", "uid": "grafana" }, - "enable": true, + "enable": false, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", diff --git a/grafana/provisioning/dashboards/yearn/yETH.json b/grafana/provisioning/dashboards/yearn/yETH.json new file mode 100644 index 000000000..0982ec836 --- /dev/null +++ b/grafana/provisioning/dashboards/yearn/yETH.json @@ -0,0 +1,2334 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 77, + "title": "yETH", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "Weekly APY updated once per day, takes the now time and updates it against the lp prices a week ago.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 86, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\",param=\"net_apy\"}", + "instant": false, + "range": true, + "refId": "A" + } + ], + "title": "st-yETH APY", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 5, + "y": 1 + }, + "id": 96, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\", param=\"boost\"}", + "instant": false, + "range": true, + "refId": "A" + } + ], + "title": "% of yETH staked into st-yETH", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 10, + "y": 1 + }, + "id": 99, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\",param=\"tvl\"}", + "instant": false, + "range": true, + "refId": "A" + } + ], + "title": "Total Value Locked in st-yETH", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 15, + "y": 1 + }, + "id": 95, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\", param=\"totalSupply\"}", + "instant": false, + "range": true, + "refId": "A" + } + ], + "title": "Amount of st-yETH", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 108, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\", param=\"token price\"}", + "instant": false, + "range": true, + "refId": "A" + } + ], + "title": "st-yETH Price", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "Boost is how much yETH is staked into st-yETH custom amm. If more yETH goes to lp in the curve yETH-eth pool then less % is staked in st-yETH causing the boost to increase. \nIf pool_supply < total_assets boost > 100%", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "series", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.spanNulls", + "value": true + }, + { + "id": "custom.axisLabel", + "value": "% ETH staked" + }, + { + "id": "color", + "value": { + "fixedColor": "light-purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "custom.axisLabel", + "value": "st-yETH APY" + }, + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 5 + }, + "id": 97, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\",param=\"net_apy\"}", + "instant": false, + "legendFormat": "{{param}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\", param=\"boost\"}", + "hide": false, + "instant": false, + "legendFormat": "% yETH staked in st-yETH", + "range": true, + "refId": "B" + } + ], + "title": "st-yETH APY vs % Staked in st-yETH", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "series", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.axisLabel", + "value": "Ether" + }, + { + "id": "unit", + "value": "none" + }, + { + "id": "color", + "value": { + "fixedColor": "#ffffff", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "custom.axisLabel", + "value": "TVL" + }, + { + "id": "color", + "value": { + "fixedColor": "#28cac2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 5 + }, + "id": 98, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\",param=\"totalSupply\"}", + "hide": false, + "instant": false, + "legendFormat": "{{product}} {{param}} ", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=\"st-yETH\",param=\"tvl\"}", + "hide": false, + "instant": false, + "legendFormat": "{{product}} {{param}} ", + "range": true, + "refId": "A" + } + ], + "title": "Total Supply of st-yETH in the AMM pool", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 68, + "panels": [], + "title": "st-yETH LSTs", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "Amount of ETH in the st-yETH amm for $lst.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4.8, + "x": 0, + "y": 17 + }, + "id": 20, + "maxPerRow": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "10.0.1", + "repeat": "lst", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{param=\"virtual_balance\", product=\"$lst\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "$lst virtual balance", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 24, + "w": 3, + "x": 0, + "y": 20 + }, + "id": 48, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "sort_desc(yeth{product!=\"st-yETH\", param=\"virtual_balance\"} / sum(yeth{product!=\"st-yETH\", param=\"virtual_balance\"}))", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "LST composition percentage", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 35, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 15, + "x": 3, + "y": 20 + }, + "id": 19, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "sort_desc(yeth{param=\"virtual_balance\"})", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "LST composition percentage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 2, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 6, + "x": 18, + "y": 20 + }, + "id": 7, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": false, + "values": [ + "percent", + "value" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "asc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product!=\"st-yETH\", param=\"virtual_balance\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "LST composition percentage", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 21, + "x": 3, + "y": 32 + }, + "id": 16, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=~\"$lst\", param=\"virtual_balance\"} * on(product) yeth{product=~\"$lst\", param=\"rate\"} ", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "LST composition in ETH", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 14, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=~\"$lst\",param=\"rate\"}", + "hide": false, + "instant": false, + "legendFormat": "rate: {{product}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=~\"$lst\",param=\"virtual_balance\"}", + "hide": false, + "instant": false, + "legendFormat": "virtual_balance: {{product}}", + "range": true, + "refId": "A" + } + ], + "title": "Virtual balance vs rate in the st-yETH AMM pool", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 18, + "x": 0, + "y": 55 + }, + "id": 17, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=~\"$lst\", param=\"target\"} - on(product) yeth{product=~\"$lst\", param=\"weight\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "Diff target-weight", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 24, + "w": 3, + "x": 18, + "y": 55 + }, + "id": 59, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "sort_desc(yeth{product=~\"$lst\", param=\"target\"})", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "LST composition target", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 24, + "w": 3, + "x": 21, + "y": 55 + }, + "id": 58, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "sort_desc(yeth{product=~\"$lst\", param=\"weight\"})", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "LST composition weight", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "left", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 13, + "w": 18, + "x": 0, + "y": 66 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=~\"$lst\", param=\"target\"}", + "hide": false, + "instant": false, + "legendFormat": "{{param}}: {{product}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product=~\"$lst\", param=\"weight\"}", + "hide": false, + "instant": false, + "legendFormat": "{{param}}: {{product}}", + "range": true, + "refId": "B" + } + ], + "title": "Target vs weight", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 100, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 7, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple" + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "sum daily swap volume outgoing" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sum Daily swap volume incoming" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-blue", + "mode": "fixed" + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Sum Daily swap volume incoming" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 79 + }, + "id": 57, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "sum(yeth{param=\"volume_in_usd\",product=~\"$lst\"})", + "instant": false, + "legendFormat": "Sum Daily swap volume incoming", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "sum(yeth{param=\"volume_out_usd\",product=~\"$lst\"})", + "hide": false, + "instant": false, + "legendFormat": "sum daily swap volume outgoing", + "range": true, + "refId": "B" + } + ], + "title": "Summed swap volume", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ETH" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 89 + }, + "id": 8, + "options": { + "barRadius": 0, + "barWidth": 1, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "normal", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xField": "Time", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 100 + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{param=\"volume_in_eth\",product=~\"$lst\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "Daily swap volume incoming ETH", + "type": "barchart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "ETH" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 89 + }, + "id": 9, + "options": { + "barRadius": 0, + "barWidth": 1, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "normal", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xField": "Time", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 100 + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{param=\"volume_out_eth\",product=~\"$lst\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "Daily swap volume outgoing ETH", + "type": "barchart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 100 + }, + "id": 10, + "options": { + "barRadius": 0, + "barWidth": 1, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "normal", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xField": "Time", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 100 + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{param=\"volume_in_usd\",product=~\"$lst\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "Daily swap volume incoming USD", + "type": "barchart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 100 + }, + "id": 11, + "options": { + "barRadius": 0, + "barWidth": 1, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "normal", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xField": "Time", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 100 + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{param=\"volume_out_usd\",product=~\"$lst\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "Daily swap volume outgoing USD", + "type": "barchart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 111 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{param=\"net_apy\", product=\"st-yETH\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "description": "APY of $lst", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 4.8, + "x": 0, + "y": 115 + }, + "id": 30, + "maxPerRow": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "10.0.1", + "repeat": "lst", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{param=\"gross_apr\", product=\"$lst\", product!=\"st-yETH\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 121 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "editorMode": "code", + "expr": "yeth{product!=\"st-yETH\", product=~\"$lst\",param=\"net_apy\"}", + "instant": false, + "legendFormat": "{{product}}", + "range": true, + "refId": "A" + } + ], + "title": "LST APY composition", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": ".*", + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "DS_PROMETHEUS" + }, + "definition": "yeth{product!=\"st-yETH\"}", + "hide": 0, + "includeAll": true, + "label": "LST", + "multi": true, + "name": "lst", + "options": [], + "query": { + "query": "yeth{product!=\"st-yETH\"}", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "/product=\"(?[^\"]+)|product=\"(?[^\"]+)/g", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-90d", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "yETH", + "uid": "eef3a622-567d-4587-8d2e-358039eb97e1", + "version": 14, + "weekStart": "" +} \ No newline at end of file diff --git a/scripts/exporters/yeth.py b/scripts/exporters/yeth.py new file mode 100644 index 000000000..c8f1fbdfd --- /dev/null +++ b/scripts/exporters/yeth.py @@ -0,0 +1,62 @@ +import json +import logging +from datetime import datetime, timezone +from typing import List + +import sentry_sdk +from brownie import chain +from y import Contract, Network +from y.datatypes import Block +from y.time import closest_block_after_timestamp + +from yearn.yeth import Registry +from yearn import constants +from yearn.helpers.exporter import Exporter +from yearn.outputs.victoria.victoria import _build_item, _post +from yearn.outputs.victoria.output_helper import _get_string_label + +sentry_sdk.set_tag('script','yeth_exporter') + +logger = logging.getLogger('yearn.yeth_exporter') + +start = { + # no yETH pool data before 2023-09-08 + Network.Mainnet: datetime(2023, 9, 8, 0, tzinfo=timezone.utc) +} + +def main(): + Exporter( + name = "yeth", + data_query = 'yeth{network="ETH"}', + data_fn = YETH().metrics_for_export, + export_fn = _post, + start_block = closest_block_after_timestamp(start[chain.id]), + concurrency=constants.CONCURRENCY, + resolution='1d', + ).run() + +class YETH: + def __init__(self): + if chain.id != Network.Mainnet: + raise NotImplementedError(f"Only supports Ethereum Mainnet") + self.registry = Registry() + + async def describe(self, block=None): + return await self.registry.describe(block) + + + async def metrics_for_export(self, block: Block, timestamp: int) -> List: + metrics_to_export = [] + data = await self.describe(block) + for product, params in data.items(): + for key, value in params.items(): + address = _get_string_label(params, "address") + item = _build_item( + "yeth", + ["product", "param", "address"], + [product, key, address], + value, + timestamp, + ) + metrics_to_export.append(item) + return metrics_to_export diff --git a/scripts/yeth.py b/scripts/yeth.py new file mode 100644 index 000000000..6f6aed7af --- /dev/null +++ b/scripts/yeth.py @@ -0,0 +1,214 @@ +import asyncio +import dataclasses +import json +import logging +import os +import shutil +import traceback +import warnings +from datetime import datetime +from time import time + +import boto3 +import requests +import sentry_sdk +from brownie import chain +from brownie.exceptions import BrownieEnvironmentWarning +from telegram.error import BadRequest +from y import Network +from y.contracts import contract_creation_block_async +from y.exceptions import yPriceMagicError + +from yearn import logs +from yearn.apy import (Apy, ApyBlocks, ApyFees, ApyPoints, ApySamples, + get_samples) +from yearn.exceptions import EmptyS3Export +from yearn.yeth import StYETH + +sentry_sdk.set_tag('script','yeth') + +warnings.simplefilter("ignore", BrownieEnvironmentWarning) + +logs.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +async def _create_apy_object(label, product, samples, aliases, icon_url) -> dict: + inception_fut = asyncio.create_task(contract_creation_block_async(product.address)) + apy_fut = asyncio.create_task(get_apy(product, samples)) + tvl_fut = asyncio.create_task(product.tvl()) + + alias = aliases[product.address]["symbol"] if product.address in aliases else product.symbol + + object = { + "inception": await inception_fut, + "address": product.address, + "symbol": product.symbol, + "name": product.name, + "display_name": alias, + "icon": icon_url % product.address, + "tvl": dataclasses.asdict(await tvl_fut), + "apy": dataclasses.asdict(await apy_fut), + "decimals": product.decimals, + "type": label, + "updated": int(time()) + } + return object + + +async def wrap_stYETH( + stYETH: StYETH, samples: ApySamples, aliases: dict, icon_url: str +) -> dict: + + stYETHobject = await _create_apy_object("st-YETH", stYETH, samples, aliases, icon_url) + lstObjects = [] + for lst in stYETH.lsts: + lstObject = await _create_apy_object("yETH LST", lst, samples, aliases, icon_url) + lstObject["asset_id"] = lst.asset_id + lstObjects.append(lstObject) + + stYETHobject["lsts"] = lstObjects + return stYETHobject + + +async def get_apy(product, samples) -> Apy: + try: + return await product.apy(samples) + except (ValueError, yPriceMagicError) as error: + logger.error(error) + return Apy( + type="error", + gross_apr=0, + net_apy=0, + fees=ApyFees(0, 0), + points=ApyPoints(0, 0, 0), + blocks=ApyBlocks(samples.now, 0, 0, 0), + error_reason=":".join(str(arg) for arg in error.args), + ) + +def main(): + asyncio.get_event_loop().run_until_complete(_main()) + + +async def _main(): + aliases_repo_url = "https://api.github.com/repos/yearn/yearn-assets/git/refs/heads/master" + aliases_repo = requests.get(aliases_repo_url).json() + + if "object" not in aliases_repo: + raise KeyError(f"key 'object' not found in {aliases_repo}") + + commit = aliases_repo["object"]["sha"] + + icon_url = f"https://rawcdn.githack.com/yearn/yearn-assets/{commit}/icons/multichain-tokens/{chain.id}/%s/logo-128.png" + + aliases_url = "https://raw.githubusercontent.com/yearn/yearn-assets/master/icons/aliases.json" + aliases = requests.get(aliases_url).json() + aliases = {alias["address"]: alias for alias in aliases} + + samples = get_samples() + + to_export = [await wrap_stYETH(StYETH(), samples, aliases, icon_url)] + + if len(to_export) == 0: + raise EmptyS3Export(f"No data for vaults was found in generated data, aborting upload") + + file_name, s3_path = _get_export_paths() + _export(to_export, file_name, s3_path) + + +def _export(data, file_name, s3_path): + print(json.dumps(data, indent=4)) + + with open(file_name, "w+") as f: + json.dump(data, f) + + if os.getenv("DEBUG", None): + return + + for item in _get_s3s(): + s3 = item["s3"] + aws_bucket = item["aws_bucket"] + s3.upload_file( + file_name, + aws_bucket, + s3_path, + ExtraArgs={'ContentType': "application/json", 'CacheControl': "max-age=1800"}, + ) + + +def _get_s3s(): + s3s = [] + aws_buckets = os.environ.get("AWS_BUCKET").split(";") + aws_endpoint_urls = os.environ.get("AWS_ENDPOINT_URL").split(";") + aws_keys = os.environ.get("AWS_ACCESS_KEY").split(";") + aws_secrets = os.environ.get("AWS_ACCESS_SECRET").split(";") + + for i in range(len(aws_buckets)): + aws_bucket = aws_buckets[i] + aws_endpoint_url = aws_endpoint_urls[i] + aws_key = aws_keys[i] + aws_secret = aws_secrets[i] + kwargs = {} + if aws_endpoint_url is not None: + kwargs["endpoint_url"] = aws_endpoint_url + if aws_key is not None: + kwargs["aws_access_key_id"] = aws_key + if aws_secret is not None: + kwargs["aws_secret_access_key"] = aws_secret + + s3s.append( + { + "s3": boto3.client("s3", **kwargs), + "aws_bucket": aws_bucket + } + ) + + return s3s + + +def _get_export_paths(): + suffix = "all" + out = "generated" + if os.path.isdir(out): + shutil.rmtree(out) + os.makedirs(out, exist_ok=True) + + yeth_api_path = os.path.join("v1", "chains", f"{chain.id}", "yeth") + file_base_path = os.path.join(out, yeth_api_path) + os.makedirs(file_base_path, exist_ok=True) + + file_name = os.path.join(file_base_path, suffix) + s3_path = os.path.join(yeth_api_path, suffix) + return file_name, s3_path + + +def with_monitoring(): + if os.getenv("DEBUG", None): + main() + return + from telegram.ext import Updater + + private_group = os.environ.get('TG_YFIREBOT_GROUP_INTERNAL') + public_group = os.environ.get('TG_YFIREBOT_GROUP_EXTERNAL') + updater = Updater(os.environ.get('TG_YFIREBOT')) + now = datetime.now() + message = f"`[{now}]`\n⚙️ YETH API for {Network.name()} is updating..." + ping = updater.bot.send_message(chat_id=private_group, text=message, parse_mode="Markdown") + ping = ping.message_id + try: + main() + except Exception as error: + tb = traceback.format_exc() + now = datetime.now() + message = f"`[{now}]`\n🔥 YETH API update for {Network.name()} failed!\n" + try: + detail_message = (message + f"```\n{tb}\n```")[:4000] + updater.bot.send_message(chat_id=private_group, text=detail_message, parse_mode="Markdown", reply_to_message_id=ping) + updater.bot.send_message(chat_id=public_group, text=detail_message, parse_mode="Markdown") + except BadRequest: + pass + #detail_message = message + f"{error.__class__.__name__}({error})" + #updater.bot.send_message(chat_id=private_group, text=detail_message, parse_mode="Markdown", reply_to_message_id=ping) + #updater.bot.send_message(chat_id=public_group, text=detail_message, parse_mode="Markdown") + raise error + message = f"✅ YETH API update for {Network.name()} successful!" + updater.bot.send_message(chat_id=private_group, text=message, reply_to_message_id=ping) diff --git a/services/dashboard/docker-compose.yml b/services/dashboard/docker-compose.yml index 15aac5c35..8c09fd5da 100644 --- a/services/dashboard/docker-compose.yml +++ b/services/dashboard/docker-compose.yml @@ -39,7 +39,7 @@ x-envs: &envs # NOTE: This is included as an escape-hatch-of-last-resort and should not actually occur - DANKMIDS_STUCK_CALL_TIMEOUT # dank-mids (optional) - - MAX_JSONRPC_BATCH_SIZE + - DANKMIDS_MAX_JSONRPC_BATCH_SIZE - DANKMIDS_BROWNIE_ENCODER_SEMAPHORE - DANKMIDS_DEMO_MODE - DANKMIDS_ETH_GETTRANSACTION_SEMAPHORE=${DANKMIDS_ETH_GETTRANSACTION_SEMAPHORE:-100} diff --git a/yearn/apy/common.py b/yearn/apy/common.py index 705052124..dd1c9d1e6 100644 --- a/yearn/apy/common.py +++ b/yearn/apy/common.py @@ -57,6 +57,7 @@ class ApySamples: now: int week_ago: int month_ago: int + day_ago: int class ApyError(ValueError): @@ -84,6 +85,7 @@ def get_samples(now_time: Optional[datetime] = None) -> ApySamples: now = web3.eth.block_number else: now = closest_block_after_timestamp(int(now_time.timestamp()), True) + day_ago = closest_block_after_timestamp(int((now_time - timedelta(days=1)).timestamp()), True) week_ago = closest_block_after_timestamp(int((now_time - timedelta(days=7)).timestamp()), True) month_ago = closest_block_after_timestamp(int((now_time - timedelta(days=31)).timestamp()), True) - return ApySamples(now, week_ago, month_ago) + return ApySamples(now, week_ago, month_ago, day_ago) diff --git a/yearn/helpers/exporter.py b/yearn/helpers/exporter.py index eb4e502f2..224745280 100644 --- a/yearn/helpers/exporter.py +++ b/yearn/helpers/exporter.py @@ -40,6 +40,7 @@ def __init__( export_fn: Callable[[T], Awaitable[None]], start_block: int, concurrency: Optional[int] = 1, + resolution: Literal = RESOLUTION ) -> None: """ Required Args: @@ -66,6 +67,9 @@ def __init__( self.data_query = data_query self.start_block = start_block self.start_timestamp = datetime.fromtimestamp(chain[start_block].timestamp, tz=timezone.utc) + if resolution != RESOLUTION: + logger.warn(f"{self.full_name}: Overriding resolution with custom value '{resolution}', env has '{RESOLUTION}' !") + self._resolution = resolution self._data_fn = data_fn self._export_fn = eth_retry.auto_retry(export_fn) @@ -103,9 +107,9 @@ async def export_full(self) -> NoReturn: async def export_future(self) -> NoReturn: """ Exports all future data. This coroutine will run forever. """ - intervals = _get_intervals(self._start_time) - start = intervals[RESOLUTION]['start'] - interval = intervals[RESOLUTION]['interval'] + intervals = _get_intervals(self._start_time, self._resolution) + start = intervals[self._resolution]['start'] + interval = intervals[self._resolution]['interval'] # Bump forward to the next snapshot, as the historical coroutine will take care of this one. start = start + interval async for snapshot in _generate_snapshot_range_forward(start, interval): @@ -113,7 +117,7 @@ async def export_future(self) -> NoReturn: async def export_history(self) -> None: """ Exports all historical data. This coroutine runs for a finite duration. """ - intervals = _get_intervals(self._start_time) + intervals = _get_intervals(self._start_time, self._resolution) for resolution in intervals: snapshot_generator = _generate_snapshot_range_historical(self.start_timestamp, resolution, intervals) await asyncio.gather(*[self.export_historical_snapshot(snapshot) async for snapshot in snapshot_generator]) diff --git a/yearn/helpers/snapshots.py b/yearn/helpers/snapshots.py index 8b9d11205..a9cef1f43 100644 --- a/yearn/helpers/snapshots.py +++ b/yearn/helpers/snapshots.py @@ -56,7 +56,7 @@ def _get_last_interval_snapshots(end: datetime, resolution: Resolution, interval last_interval_snapshots.add(snapshot) return last_interval_snapshots -def _get_intervals(start): +def _get_intervals(start, resolution): # default resolution is hourly resolutions = { '1d': { @@ -92,10 +92,10 @@ def _get_intervals(start): 'interval': timedelta(seconds=15), }, } - assert RESOLUTION in resolutions, f"resolution {RESOLUTION} not supported. Must be one of {resolutions}" + assert resolution in resolutions, f"resolution {resolution} not supported. Must be one of {resolutions}" intervals = {} for res, params in resolutions.items(): intervals[res] = params - if res == RESOLUTION: + if res == resolution: break return intervals diff --git a/yearn/yearn.py b/yearn/yearn.py index 7f0ec550c..a2da17834 100644 --- a/yearn/yearn.py +++ b/yearn/yearn.py @@ -13,6 +13,7 @@ import yearn.special import yearn.v1.registry import yearn.v2.registry +import yearn.yeth from yearn.exceptions import UnsupportedNetwork from yearn.ironbank import addresses as ironbank_registries from yearn.outputs.victoria.output_helper import (_flatten_dict, @@ -37,6 +38,7 @@ def __init__(self, load_strategies=True, load_harvests=False, load_transfers=Fal "v2": yearn.v2.registry.Registry(watch_events_forever=watch_events_forever), "ib": yearn.ironbank.Registry(exclude_ib_tvl=exclude_ib_tvl), "special": yearn.special.Registry(), + "yeth": yearn.yeth.Registry(), } elif chain.id in [Network.Gnosis, Network.Base]: self.registries = { diff --git a/yearn/yeth.py b/yearn/yeth.py new file mode 100644 index 000000000..b8704afd2 --- /dev/null +++ b/yearn/yeth.py @@ -0,0 +1,275 @@ +import asyncio +import os +import re +import logging +from datetime import datetime, timezone + +import eth_retry + +from brownie import chain +from pprint import pformat + +from y import Contract, magic, Network +from y.time import get_block_timestamp +from y.contracts import contract_creation_block_async +from y.exceptions import PriceError, yPriceMagicError + +from yearn.apy.common import (Apy, ApyFees, + ApySamples, SECONDS_PER_YEAR, SECONDS_PER_WEEK, SharePricePoint, calculate_roi, get_samples) +from yearn.common import Tvl +from yearn.events import decode_logs, get_logs_asap +from yearn.utils import Singleton +from yearn.prices.constants import weth +from yearn.debug import Debug + +logger = logging.getLogger("yearn.yeth") + +if chain.id == Network.Mainnet: + YETH_POOL = Contract("0x2cced4ffA804ADbe1269cDFc22D7904471aBdE63") + RATE_PROVIDER = Contract("0x4e322aeAf355dFf8fb9Fd5D18F3D87667E8f8316") + STAKING_CONTRACT = Contract("0x583019fF0f430721aDa9cfb4fac8F06cA104d0B4") # st-yETH + YETH_TOKEN = Contract("0x1BED97CBC3c24A4fb5C069C6E311a967386131f7") # yETH + +class StYETH(metaclass = Singleton): + def __init__(self): + self.name = "st-yETH" + self.address = str(STAKING_CONTRACT) + self.lsts = [] + # TODO load assets via events + for i in range(YETH_POOL.num_assets()): + address = YETH_POOL.assets(i) + lst = YETHLST(address, i) + self.lsts.append(lst) + + @property + def decimals(self): + return 18 + + @property + def symbol(self): + return 'st-yETH' + + + async def _get_supply_price(self, block=None): + data = YETH_POOL.vb_prod_sum(block_identifier=block) + supply = data[1] / 1e18 + try: + price = await magic.get_price(YETH_TOKEN, block=block, sync=False) + except yPriceMagicError as e: + if not isinstance(e.exception, PriceError): + raise e + price = None + + return supply, price + + + @eth_retry.auto_retry + async def apy(self, samples: ApySamples) -> Apy: + block = samples.now + now = get_block_timestamp(block) + seconds_til_eow = SECONDS_PER_WEEK - now % SECONDS_PER_WEEK + + data = STAKING_CONTRACT.get_amounts(block_identifier=block) + streaming = data[1] + unlocked = data[2] + apr = streaming * SECONDS_PER_YEAR / seconds_til_eow / unlocked + performance_fee = STAKING_CONTRACT.performance_fee_rate(block_identifier=block) / 1e4 + + if os.getenv("DEBUG", None): + logger.info(pformat(Debug().collect_variables(locals()))) + + return Apy("yETH", gross_apr=apr, net_apy=apr, fees=ApyFees(performance=performance_fee)) + + + @eth_retry.auto_retry + async def tvl(self, block=None) -> Tvl: + supply, price = await self._get_supply_price(block=block) + tvl = supply * price if price else None + + return Tvl(supply, price, tvl) + + + async def describe(self, block=None): + supply, price = await self._get_supply_price(block=block) + try: + pool_supply = YETH_POOL.supply(block_identifier=block) + total_assets = STAKING_CONTRACT.totalAssets(block_identifier=block) + boost = total_assets / pool_supply + except Exception as e: + logger.error(e) + boost = 0 + + if block: + block_timestamp = get_block_timestamp(block) + samples = get_samples(datetime.fromtimestamp(block_timestamp)) + else: + samples = get_samples() + + apy = await self.apy(samples) + return { + 'address': self.address, + 'totalSupply': supply, + 'token price': price, + 'tvl': supply * price, + 'net_apy': apy.net_apy, + 'gross_apr': apy.gross_apr, + 'boost': boost + } + + + async def total_value_at(self, block=None): + supply, price = await self._get_supply_price(block=block) + return supply * price + + +class YETHLST(): + def __init__(self, address, asset_id): + self.asset_id = asset_id + self.address = address + self.lst = Contract(address) + self.name = self._sanitize(self.lst.name()) + + @property + def symbol(self): + return self.lst.symbol() + + @property + def decimals(self): + return 18 + + def _sanitize(self, name): + return re.sub(r"([\d]+)\.[\d]*", r"\1", name) + + def _get_lst_data(self, block=None): + virtual_balance = YETH_POOL.virtual_balance(self.asset_id, block_identifier=block) / 1e18 + weights = YETH_POOL.weight(self.asset_id, block_identifier=block) + weight = weights[0] / 1e18 + target = weights[1] / 1e18 + rate = RATE_PROVIDER.rate(str(self.lst), block_identifier=block) / 1e18 + + return { + "virtual_balance": virtual_balance, + "weight": weight, + "target": target, + "rate": rate + } + + @eth_retry.auto_retry + async def apy(self, samples: ApySamples) -> Apy: + now_rate = RATE_PROVIDER.rate(str(self.lst), block_identifier=samples.now) / 1e18 + week_ago_rate = RATE_PROVIDER.rate(str(self.lst), block_identifier=samples.week_ago) / 1e18 + now_point = SharePricePoint(samples.now, now_rate) + week_ago_point = SharePricePoint(samples.week_ago, week_ago_rate) + apy = calculate_roi(now_point, week_ago_point) + + return Apy("yETH", gross_apr=apy, net_apy=apy, fees=ApyFees()) + + @eth_retry.auto_retry + async def tvl(self, block=None) -> Tvl: + data = self._get_lst_data(block=block) + tvl = data["virtual_balance"] * data["rate"] + return Tvl(data["virtual_balance"], data["rate"], tvl) + + async def describe(self, block=None): + weth_price = await magic.get_price(weth, block=block, sync=False) + data = self._get_lst_data(block=block) + + if block: + block_timestamp = get_block_timestamp(block) + samples = get_samples(datetime.fromtimestamp(block_timestamp)) + else: + samples = get_samples() + + apy = await self.apy(samples) + + registry = Registry() + swap_volumes = registry.swap_volumes + + return { + 'address': self.address, + 'weth_price': weth_price, + 'virtual_balance': data["virtual_balance"], + 'rate': data["rate"], + 'weight': data["weight"], + 'target': data["target"], + 'net_apy': apy.net_apy, + 'gross_apr': apy.gross_apr, + 'volume_in_eth': swap_volumes['volume_in_eth'][self.asset_id], + 'volume_out_eth': swap_volumes['volume_out_eth'][self.asset_id], + 'volume_in_usd': swap_volumes['volume_in_usd'][self.asset_id], + 'volume_out_usd': swap_volumes['volume_out_usd'][self.asset_id] + } + + async def total_value_at(self, block=None): + data = self._get_lst_data(block=block) + tvl = data["virtual_balance"] * data["rate"] + return tvl + + +class Registry(metaclass = Singleton): + def __init__(self) -> None: + self.st_yeth = StYETH() + self.swap_volumes = {} + + async def _get_daily_swap_volumes(self, from_block, to_block): + logs = get_logs_asap([str(YETH_POOL)], None, from_block=from_block, to_block=to_block) + events = decode_logs(logs) + + num_assets = YETH_POOL.num_assets(block_identifier=from_block) + volume_in_eth = [0] * num_assets + volume_out_eth = [0] * num_assets + volume_in_usd = [0] * num_assets + volume_out_usd = [0] * num_assets + + rates = [] + for i in range(num_assets): + lst = self.st_yeth.lsts[i] + address = str(lst.lst) + rates.append(RATE_PROVIDER.rate(address, block_identifier=from_block) / 1e18) + + for e in events: + if e.name == "Swap": + asset_in = e["asset_in"] + asset_out = e["asset_out"] + amount_in = e["amount_in"] / 1e18 + amount_out = e["amount_out"] / 1e18 + volume_in_eth[asset_in] += amount_in * rates[asset_in] + volume_out_eth[asset_out] += amount_out * rates[asset_out] + + weth_price = await magic.get_price(weth, block=from_block, sync=False) + for i, value in enumerate(volume_in_eth): + volume_in_usd[i] = value * weth_price + + for i, value in enumerate(volume_out_eth): + volume_out_usd[i] = value * weth_price + + return { + "volume_in_eth": volume_in_eth, + "volume_out_eth": volume_out_eth, + "volume_in_usd": volume_in_usd, + "volume_out_usd": volume_out_usd + } + + async def describe(self, block=None): + if block: + block_timestamp = get_block_timestamp(block) + samples = get_samples(datetime.fromtimestamp(block_timestamp)) + else: + samples = get_samples() + + self.swap_volumes = await self._get_daily_swap_volumes(samples.day_ago, samples.now) + products = await self.active_products_at(block) + data = await asyncio.gather(*[product.describe(block=block) for product in products]) + return {product.name: desc for product, desc in zip(products, data)} + + async def total_value_at(self, block=None): + products = await self.active_products_at(block) + tvls = await asyncio.gather(*[product.total_value_at(block=block) for product in products]) + return {product.name: tvl for product, tvl in zip(products, tvls)} + + async def active_products_at(self, block=None): + products = [self.st_yeth] + self.st_yeth.lsts + if block: + blocks = await asyncio.gather(*[contract_creation_block_async(str(product.address)) for product in products]) + products = [product for product, deploy_block in zip(products, blocks) if deploy_block <= block] + return products