From bff72ff5b572ac791089ea94689b763b1367d281 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sun, 28 Jan 2024 22:23:08 +0100 Subject: [PATCH] Add Solana market data to wallet tracker --- README.md | 2 +- config.yaml | 22 +- ctl.sh | 14 +- docker-compose.yaml | 6 + grafana/dashboards/drift-keeper.json | 562 ++++++++++++++++++++++++++- prometheus/prometheus.yml | 8 +- wallet-tracker/src/main.js | 16 +- wallet-tracker/src/metrics.js | 10 +- wallet-tracker/src/solana.js | 15 +- 9 files changed, 626 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index af8db10..347230f 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Adjust `config.yaml` as you please. Clone the [keeper-bots-v2](https://github.com/drift-labs/keeper-bots-v2/) repository and build the Docker image. ``` -./ctl.sh image build +./ctl.sh keeper build ``` ## Run diff --git a/config.yaml b/config.yaml index 8df4a51..00125ee 100644 --- a/config.yaml +++ b/config.yaml @@ -4,9 +4,29 @@ global: # RPC endpoint to use endpoint: - # custom websocket endpoint to use (if null will be determined from rpc endpoint) + + # Custom websocket endpoint to use (if null will be determined from `endpoint``) + # Note: the default wsEndpoint value simply replaces http(s) with ws(s), so if + # your RPC provider requires a special path (i.e. /ws) for websocket connections + # you must set this. wsEndpoint: + # optional if you want to use helius' global priority fee method AND `endpoint` is not + # already a helius url. + heliusEndpoint: + + # `solana` or `helius`. If `helius` `endpoint` must be a helius RPC, or `heliusEndpoint` + # must be set + # solana: uses https://solana.com/docs/rpc/http/getrecentprioritizationfees + # helius: uses https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api + priorityFeeMethod: solana + + # max priority fee to use, in micro lamports + # i.e. a fill that uses 500_000 CUs will spend: + # 500_000 * 10_000 * 1e-6 * 1e-9 = 0.000005 SOL on priority fees + # this is on top of the 0.000005 SOL base fee, so 0.000010 SOL total + maxPriorityFeeMicroLamports: 10000 + # Private key to use to sign transactions. # will load from KEEPER_PRIVATE_KEY env var if null keeperPrivateKey: diff --git a/ctl.sh b/ctl.sh index 8c11f68..b3f7218 100755 --- a/ctl.sh +++ b/ctl.sh @@ -5,7 +5,7 @@ API_ENDPOINT=https://api.mainnet-beta.solana.com/ source .env -function image { +function keeper { function build { mkdir -p .build git clone https://github.com/drift-labs/keeper-bots-v2 -b mainnet-beta .build/keeper-bots-v2 @@ -18,6 +18,18 @@ function image { ${@:-} } +function tracker { + function build { + pushd wallet-tracker + docker build -t ${DOCKER_IMAGE_WALLET_TRACKER} . + popd + } + function push { + docker push ${DOCKER_IMAGE_WALLET_TRACKER} + } + ${@:-} +} + function droplet { function provision { terraform init diff --git a/docker-compose.yaml b/docker-compose.yaml index d884518..7051acc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,6 +15,12 @@ services: # --------------------------------------------------- # Monitoring # --------------------------------------------------- + wallet-tracker: + image: ${DOCKER_IMAGE_WALLET_TRACKER} + build: + context: wallet-tracker + env_file: .env + restart: unless-stopped prometheus: image: prom/prometheus container_name: prometheus diff --git a/grafana/dashboards/drift-keeper.json b/grafana/dashboards/drift-keeper.json index ed3a0c0..1a49037 100644 --- a/grafana/dashboards/drift-keeper.json +++ b/grafana/dashboards/drift-keeper.json @@ -64,7 +64,7 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 5, "w": 4, "x": 0, "y": 1 @@ -136,7 +136,7 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 5, "w": 4, "x": 4, "y": 1 @@ -208,7 +208,7 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 5, "w": 4, "x": 8, "y": 1 @@ -251,6 +251,225 @@ "title": "Unrealized PNL", "type": "stat" }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "SOL" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 12, + "y": 1 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sol_balance", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "SOL Balance", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 16, + "y": 1 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sol_usdc_balance", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "SOL Balance in USDC", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 22, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "usdc_balance", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "USDC Balance", + "type": "stat" + }, { "datasource": { "type": "prometheus", @@ -273,10 +492,10 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 1 + "h": 6, + "w": 24, + "x": 0, + "y": 6 }, "id": 12, "options": { @@ -356,7 +575,318 @@ "h": 1, "w": 24, "x": 0, - "y": 8 + "y": 12 + }, + "id": 17, + "panels": [], + "title": "Wallet", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 13 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sol_balance", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "SOL Balance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 13 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sol_usdc_balance", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "SOL Balance in USDC", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 13 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "usdc_balance", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "USDC Balance", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 }, "id": 11, "panels": [], @@ -428,7 +958,7 @@ "h": 13, "w": 12, "x": 0, - "y": 9 + "y": 22 }, "id": 7, "options": { @@ -530,7 +1060,7 @@ "h": 13, "w": 12, "x": 12, - "y": 9 + "y": 22 }, "id": 8, "options": { @@ -573,7 +1103,7 @@ "h": 1, "w": 24, "x": 0, - "y": 22 + "y": 35 }, "id": 10, "panels": [], @@ -644,7 +1174,7 @@ "h": 12, "w": 12, "x": 0, - "y": 23 + "y": 36 }, "id": 15, "options": { @@ -748,7 +1278,7 @@ "h": 12, "w": 12, "x": 12, - "y": 23 + "y": 36 }, "id": 1, "options": { @@ -831,7 +1361,7 @@ "h": 12, "w": 12, "x": 0, - "y": 35 + "y": 48 }, "id": 5, "options": { @@ -910,7 +1440,7 @@ "h": 12, "w": 12, "x": 12, - "y": 35 + "y": 48 }, "id": 14, "options": { @@ -961,6 +1491,6 @@ "timezone": "", "title": "Drift Keeper", "uid": "f4c3a630-ffd0-41c8-a36a-52e0771b77fb", - "version": 3, + "version": 6, "weekStart": "" } \ No newline at end of file diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml index 589cabf..8a85015 100644 --- a/prometheus/prometheus.yml +++ b/prometheus/prometheus.yml @@ -1,5 +1,5 @@ global: - scrape_interval: 15s + scrape_interval: 60s scrape_timeout: 10s evaluation_interval: 15s alerting: @@ -13,6 +13,12 @@ scrape_configs: - job_name: node static_configs: - targets: ['node-exporter:9100'] +- job_name: wallet + scrape_interval: 60s + metrics_path: /metrics + static_configs: + - targets: + - wallet-tracker:3000 - job_name: keeper honor_timestamps: true scrape_interval: 15s diff --git a/wallet-tracker/src/main.js b/wallet-tracker/src/main.js index 0c92077..fbacb26 100644 --- a/wallet-tracker/src/main.js +++ b/wallet-tracker/src/main.js @@ -1,21 +1,23 @@ const express = require('express'); const { createMetrics } = require('./metrics'); -const { loadWalletBalance, loadUSDCBalance, extractWalletBalance, extractUSDCBalance } = require('./solana'); +const { loadWalletBalance, loadUSDCBalance, loadSolanaMarketData, extractWalletBalance, extractUSDCBalance, extractSOLPrice } = require('./solana'); const WALLET_ADDRESS = process.env.WALLET_ADDRESS; -const [registry, usdcBalanceMetric, solBalanceMetric] = createMetrics(); +const [registry, usdcBalanceMetric, solBalanceMetric, solUsdcBalanceMetric] = createMetrics(); const app = express(); app.get('/metrics', async (req, res) => { res.setHeader('Content-Type', registry.contentType); - let [solBalance, usdcBalance] = await Promise.all([ + let [solBalance, usdcBalance, marketData] = await Promise.all([ loadWalletBalance(WALLET_ADDRESS), - loadUSDCBalance(WALLET_ADDRESS)]); - - solBalanceMetric.set(extractWalletBalance(solBalance)); - usdcBalanceMetric.set(extractUSDCBalance(usdcBalance)); + loadUSDCBalance(WALLET_ADDRESS), + loadSolanaMarketData()]); + solBalanceMetric.set({ wallet: WALLET_ADDRESS}, extractWalletBalance(solBalance)); + usdcBalanceMetric.set({ wallet: WALLET_ADDRESS}, extractUSDCBalance(usdcBalance)); + solUsdcBalanceMetric.set({ wallet: WALLET_ADDRESS}, extractWalletBalance(solBalance) * extractSOLPrice(marketData)); + res.send(await registry.metrics()); }); diff --git a/wallet-tracker/src/metrics.js b/wallet-tracker/src/metrics.js index f1e583d..385551c 100644 --- a/wallet-tracker/src/metrics.js +++ b/wallet-tracker/src/metrics.js @@ -6,16 +6,24 @@ const createMetrics = () => { const solBalanceMetric = new client.Gauge({ name: "sol_balance", help: "SOL Balance", + labelNames: ['wallet'] }); const usdcBalanceMetric = new client.Gauge({ name: "usdc_balance", help: "USDC Balance", + labelNames: ['wallet'] + }); + const solUsdcBalanceMetric = new client.Gauge({ + name: "sol_usdc_balance", + help: "SOL Balance in USDC", + labelNames: ['wallet'] }); registry.registerMetric(usdcBalanceMetric); registry.registerMetric(solBalanceMetric); + registry.registerMetric(solUsdcBalanceMetric); - return [registry, usdcBalanceMetric, solBalanceMetric]; + return [registry, usdcBalanceMetric, solBalanceMetric, solUsdcBalanceMetric]; }; module.exports = { diff --git a/wallet-tracker/src/solana.js b/wallet-tracker/src/solana.js index 9ea5501..dfb6ede 100644 --- a/wallet-tracker/src/solana.js +++ b/wallet-tracker/src/solana.js @@ -39,6 +39,12 @@ async function loadUSDCBalance(walletAddress) { return json; } +async function loadSolanaMarketData() { + const response = await fetch("https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=solana"); + const json = await response.json(); + return json; +} + // extract wallet balance function extractWalletBalance(walletBalance) { return walletBalance.result.value / LAMPORTS_PER_SOL; @@ -49,10 +55,17 @@ function extractUSDCBalance(usdcBalance) { return usdcBalance.result.value[0].account.data.parsed.info.tokenAmount.uiAmount; } +// extract SOL price +function extractSOLPrice(marketData) { + return marketData[0].current_price; +} + // export functions module.exports = { loadWalletBalance, loadUSDCBalance, + loadSolanaMarketData, extractWalletBalance, - extractUSDCBalance + extractUSDCBalance, + extractSOLPrice }; \ No newline at end of file