mirror of
https://github.com/0x1d/drift-keeper.git
synced 2025-12-14 18:35:20 +01:00
Multi-Cloud Deployment (#1)
* Introduce instances list to scale bots and add config templating * Use env var for wallet address if not provided as path param * Expose SOL price as metric * Update docs * Add auto-swap * Add Panopticon * implement backoff stategy in autoswap * Add retry logic for withdraw and swap * bump drift-sdk, update ctl.sh and docs * Update filler bot, add tx metrics * Add user-metrics * Update build and dashboard
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,9 +1,12 @@
|
||||
# Bot
|
||||
.env
|
||||
.env*
|
||||
.build/
|
||||
|
||||
# Terraform
|
||||
.terraform/
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
values.auto.tfvars
|
||||
values.auto.tfvars
|
||||
|
||||
# Ansible
|
||||
inventory.cfg
|
||||
22
.terraform.lock.hcl
generated
22
.terraform.lock.hcl
generated
@@ -24,3 +24,25 @@ provider "registry.terraform.io/digitalocean/digitalocean" {
|
||||
"zh:fbe5edf5337adb7360f9ffef57d02b397555b6a89bba68d1b60edfec6e23f02c",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/linode/linode" {
|
||||
version = "2.13.0"
|
||||
constraints = "2.13.0"
|
||||
hashes = [
|
||||
"h1:bsdPJRav766kmjV62GdSnOmaZTfmSMcLNbKyWS9+5Ms=",
|
||||
"zh:25776adaba8c01251f02326b356e960f5e7ea1bf5f7e60b3b598792a0a3b5b4f",
|
||||
"zh:30bc848498bcf2ad1366ea32068c2983d982140669aa507bb7a21714fe5f6beb",
|
||||
"zh:32b5078f3defe920d611da19ff53139cbdff7de74a19238a9611d206255d5eb8",
|
||||
"zh:32b69e72ed06273550888b5fa6c53b05f37d6fb42aa898b1981e0e40d5332cd3",
|
||||
"zh:7b1df46d734b461b006f2fc92a8f8e4e810afa5458c50f2016ee99e1541f6a4b",
|
||||
"zh:7eda947ae4aefd486e758d6f86985607c9764ea55556aacf8a9fcc78780fa6d0",
|
||||
"zh:832ec3adf887bcbbff99021ca1518e44f51e1c6af0d7fe639ceddc92921df130",
|
||||
"zh:8cd1dfd9edcdd9432ce567981dc653cc2cdedf6349513614493c37485533d519",
|
||||
"zh:a5cf20563230d2180fd48a1716315f7ccfb20d8e12eceb29609135122a2a07db",
|
||||
"zh:b8d66ae8c6fbd31cea8cfc15f8160e3e00b6d79f8afcdd2b88ccbe63c3bbc34e",
|
||||
"zh:da0a28bde0bf5ac818fc07c7ab1136e7dfb46efa98fd38e1450104582d629f96",
|
||||
"zh:dc611263577f1ee319a229fb2e47d1ab5ad99cfab8eb4d43c09afcafefb00f1d",
|
||||
"zh:e23355243d5c024caec26f072035b67707f09ee1231361168919023d2ca15c65",
|
||||
"zh:e51d4813a58e79bc953c77db295e23a8abf0f8c21afcfc6401dc1010b9613a48",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
FROM public.ecr.aws/bitnami/node:16
|
||||
FROM public.ecr.aws/bitnami/node:18
|
||||
RUN apt-get install git
|
||||
ENV NODE_ENV=production
|
||||
RUN npm install -g yarn
|
||||
RUN npm install -g typescript
|
||||
RUN npm install -g ts-node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
|
||||
RUN yarn install
|
||||
COPY . .
|
||||
RUN yarn install
|
||||
RUN yarn build
|
||||
RUN yarn install --production
|
||||
|
||||
|
||||
225
README.md
225
README.md
@@ -2,13 +2,111 @@
|
||||
|
||||
> Keeper Bots in the Drift Protocol keep the protocol operational by performing automated actions as autonomous off-chain agents. Keepers are rewarded depending on the duties that they perform.
|
||||
|
||||
This repository contains tools to build, run and monitor Keeper bots for Drift on Solana.
|
||||
|
||||
This repository contains tools to build, run and monitor Drift Keeper bots on Solana.
|
||||
Checkout my article about all that stuff: https://nitro.dcentral.systems/drift-keeper-bot
|
||||
|
||||
More information:
|
||||
- https://github.com/drift-labs/keeper-bots-v2/
|
||||
- https://docs.drift.trade/keeper-bots
|
||||
- https://docs.drift.trade/tutorial-order-matching-bot
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
This repository contains several components for automation and monitoring of Drift Keeper bots:
|
||||
|
||||
- Keeper Bot
|
||||
- Wallet-Tracker
|
||||
- User-Tracker
|
||||
- Auto-Swap
|
||||
- Panopticon
|
||||
|
||||
### Keeper-Bot
|
||||
|
||||
The Keeper-Bot is the core component of this whole thing.
|
||||
It performs off-chain actions for the Drift Protocol and receives rewards in USDC. A portion of SOL is required to pay for transaction fees.
|
||||
While there are several types of bots for Drift, this repository is focused on order matching a.k.a filler bots that tries to match orders in the decentralized orderbook.
|
||||
This repository only serves as a build and deploy stack for the official bot of this source repository: https://github.com/drift-labs/keeper-bots-v2/.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
Terraform --> Linode
|
||||
Terraform --> DigitalOcean
|
||||
Linode --> Keeper_1
|
||||
Linode --> Keeper_2
|
||||
DigitalOcean --> Keeper_3
|
||||
DigitalOcean --> Keeper_4
|
||||
```
|
||||
|
||||
### Wallet-Tracker
|
||||
|
||||
As the name suggests, this component tracks the current SOL and USDC onchain balance of a given wallet, as well as the current SOL price from Jupiter.
|
||||
Everything is conveniently exportes as Prometheus metrics and will be scraped by the `Panopticon`.
|
||||
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
WalletTracker --> Solana
|
||||
WalletTracker --> Jupiter
|
||||
Solana --> USDC_Balance
|
||||
Solana --> SOL_Balance
|
||||
Jupiter --> SOL_Price
|
||||
|
||||
```
|
||||
|
||||
### User-Tracker
|
||||
|
||||
This component tracks the current SOL and USDC balance in the wallet as well as total collateral and unrealized PNL on Drift.
|
||||
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
UserTracker --> Solana
|
||||
UserTracker --> Drift
|
||||
Solana --> USDC_Balance
|
||||
Solana --> SOL_Balance
|
||||
Drift --> Total_Collateral
|
||||
Drift --> Unrealized_PNL
|
||||
|
||||
```
|
||||
|
||||
### Auto-Swap
|
||||
|
||||
The Auto-Swap is resposible of keeping the bot afloat by automatically swapping a portion of the profits to SOL in order to pay for transaction fees.
|
||||
It periodically checks if a configurable amount of USDC collateral is available on your Drift account. If this amount is reached,it withdraws all collateral and swaps a configurable portion of the profits to SOL using Jupiter.
|
||||
Altough it is possible to configure the swap ratio so that you can make profits in both SOL and USDC. this only seems to work on "slow days". With crazy market fluctuation and high trading volume, consider that you might pay a lot of fees.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> checkBalance
|
||||
checkBalance --> [*]:threshold\nnot reached
|
||||
checkBalance --> withdraw: threshold\nreached
|
||||
withdraw --> withdraw: try 3x
|
||||
withdraw --> [*]: backoff
|
||||
withdraw --> swap
|
||||
swap --> swap: try 3x
|
||||
swap --> [*]
|
||||
```
|
||||
|
||||
Known problem: when Solana is congested, the autoswap routine fails a lot, as the `withdraw` method seems to be somehow unstable.
|
||||
|
||||
### Panopticon
|
||||
|
||||
This is basically the monitoring stack consisting of a Prometheus and Grafana instance.
|
||||
As every bot is accompanied by a Prometheus instance, the Prometheus of the Panopticon is configured to scrape the `/federate` endpoint of all available instances to centralize monitoring of all servers, bots and wallets.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
Panopticon --> WalletTracker
|
||||
Panopticon --> /federate
|
||||
/federate --> NodeExporter
|
||||
/federate --> Keeper
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker, Docker-Compose
|
||||
@@ -17,58 +115,141 @@ More information:
|
||||
- Jito Private Key for auth to block engine API (optional)
|
||||
- Terraform (optional)
|
||||
- DigitalOcean API Key (optional)
|
||||
- Linode API Key (optional)
|
||||
|
||||
Drift account is setup according to: https://docs.drift.trade/keeper-bots.
|
||||
The account can also be set up using the DEX app: https://app.drift.trade/.
|
||||
|
||||
## Configure
|
||||
## Build
|
||||
|
||||
In order to build all components and push them to the Docker registry, simply run:
|
||||
|
||||
```
|
||||
./ctl.sh build all
|
||||
./ctl.sh push all
|
||||
```
|
||||
|
||||
## Run Locally
|
||||
|
||||
Create .env file from example and configure all environment variables.
|
||||
|
||||
```
|
||||
cp example.env .env
|
||||
cp example.env.monitoring .env.monitoring
|
||||
cp example.env.autoswap .env.autoswap
|
||||
cp example.env.user-metrics .env.user-metrics
|
||||
```
|
||||
|
||||
Adjust `config.yaml` as you please.
|
||||
|
||||
## Build
|
||||
|
||||
Clone the [keeper-bots-v2](https://github.com/drift-labs/keeper-bots-v2/) repository and build the Docker image.
|
||||
Adjust `config.yaml` as you please (e.g. configure Jito).
|
||||
Then just run the bot.
|
||||
|
||||
```
|
||||
./ctl.sh keeper build
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Run the bot.
|
||||
|
||||
```
|
||||
docker compose up
|
||||
./ctl.sh run
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
Provision a DigitalOcean Droplet and deploy Keeper Bot with current configuration (.env and config.yaml).
|
||||
By default ~/.ssh/id_rsa.pub is added to DigitalOcean and the Droplet.
|
||||
Provision DigitalOcean and Linode instances using Terraform.
|
||||
By default ~/.ssh/id_rsa.pub is added to the root account of each server.
|
||||
|
||||
First, create a `values.auto.tfvars` with all your secrets:
|
||||
|
||||
```
|
||||
./ctl.sh droplet provision
|
||||
do_token = "your-token"
|
||||
linode_token = "your-token"
|
||||
bot = {
|
||||
wallet_address = "your-wallet-address"
|
||||
rpc_endpoint = "https://your-endpoint"
|
||||
ws_endpoint = "wss://your-ws-endpoint"
|
||||
keeper_private_key = "[123,456...789]"
|
||||
jito_private_key = "[123,456...789]"
|
||||
}
|
||||
monitoring = {
|
||||
grafana_user = "admin"
|
||||
grafana_password = "grafana"
|
||||
prometheus_password = "prompass"
|
||||
}
|
||||
```
|
||||
|
||||
Wait until Droplet is up and the `droplet_ip` is printed. You may connect to the Droplet using
|
||||
If you want to configure custom servers, you may also add them to your values file:
|
||||
|
||||
```
|
||||
./ctl.sh droplet connect
|
||||
linode_instances = [
|
||||
{
|
||||
label = "DK-LN-AMS"
|
||||
group = "keeper"
|
||||
image = "linode/ubuntu23.10"
|
||||
region = "nl-ams"
|
||||
type = "g6-standard-1"
|
||||
ntp_server = "ntp.amsterdam.jito.wtf"
|
||||
jito_block_engine_url = "amsterdam.mainnet.block-engine.jito.wtf"
|
||||
use_jito = true
|
||||
}
|
||||
]
|
||||
digitalocean_instances = [
|
||||
{
|
||||
label = "DK-DO-FRA"
|
||||
image = "ubuntu-23-10-x64"
|
||||
region = "fra1"
|
||||
type = "s-1vcpu-1gb"
|
||||
ntp_server = "ntp.frankfurt.jito.wtf"
|
||||
jito_block_engine_url = "frankfurt.mainnet.block-engine.jito.wtf"
|
||||
use_jito = true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
If no custom instances are provided, the default set from `variables.tf` will be used:
|
||||
- Linode g6-nanode-1 in Amsterdam (NL)
|
||||
- Linode g6-nanode-1 in Osaka (JP)
|
||||
- DigitalOcean s-1vcpu-1gb in Frankfurt (DE)
|
||||
- DigitalOcean s-1vcpu-1gb in New York (US)
|
||||
|
||||
At the time of writing, this setup will cost 22$.
|
||||
|
||||
Provision the infrastructure:
|
||||
|
||||
```
|
||||
./ctl.sh infra provision
|
||||
```
|
||||
|
||||
Wait until all instances are up and the `instances` output is printed. You may connect to any server using
|
||||
|
||||
```
|
||||
./ctl.sh infra connect
|
||||
```
|
||||
|
||||
In case somethin went wrong with the provisioning, check the cloud-init-output log at `/var/log/cloud-init-output.log`.
|
||||
|
||||
## RPC Providers
|
||||
|
||||
In order for Keeper bots to run smoothly, you need to choose a suitable RPC provider that allows all method calls (e.g. getProgramAccounts, etc.).
|
||||
|
||||
Checkout https://solana.com/rpc
|
||||
|
||||
Helius and ExtrNode are quite suitable with reasonable pricing.
|
||||
|
||||
## Geo-Locating Nodes
|
||||
|
||||
When deploying multiple bots all around the globe, consider placing the machines near your RPC provider and other Solana nodes:
|
||||
https://solanacompass.com/statistics/decentralization
|
||||
|
||||
Keep in mind, not all Linode datacenters support the `Metadata Service` required to apply the cloud-init config. Check the availability here: https://www.linode.com/docs/products/compute/compute-instances/guides/metadata/#availability
|
||||
|
||||
## Maintenance
|
||||
|
||||
There are several Ansible playbooks to maintain the servers and the app that can be selected and run through the ctl.sh.
|
||||
|
||||
```
|
||||
./ctl.sh infra playbook
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
There are several metrics endpoints available that are periodically scraped by Prometheus:
|
||||
- http://keeper:9464/metrics
|
||||
- http://wallet-tracker:3000/metrics
|
||||
- http://user-tracker:3000/metrics
|
||||
- http://node-exporter:9100/metrics
|
||||
|
||||
A Grafana dashboard is exposed on http://localhost:3000 with default username/password: admin/grafana.
|
||||
|
||||
24
ansible/app-configure.yaml
Normal file
24
ansible/app-configure.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
- hosts: bots
|
||||
remote_user: root
|
||||
become: true
|
||||
tasks:
|
||||
- name: Stop Containers
|
||||
shell: |
|
||||
cd /app/bot
|
||||
docker-compose down
|
||||
- name: Configure Grafana
|
||||
copy:
|
||||
src: ../grafana
|
||||
dest: /app/bot
|
||||
#- name: Configure Prometheus
|
||||
# copy:
|
||||
# src: ../prometheus
|
||||
# dest: /app/bot
|
||||
#- name: Configure Bot
|
||||
# copy:
|
||||
# src: ../config.yaml
|
||||
# dest: /app/bot
|
||||
- name: Start Containers
|
||||
shell: |
|
||||
cd /app/bot
|
||||
sudo -u keeper -g bot -- docker-compose up -d
|
||||
9
ansible/app-reset.yaml
Normal file
9
ansible/app-reset.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
- hosts: bots
|
||||
remote_user: root
|
||||
become: true
|
||||
tasks:
|
||||
- name: Reset Containers
|
||||
shell: |
|
||||
cd /app/bot
|
||||
docker-compose down -v
|
||||
sudo -u keeper -g bot -- docker-compose up -d
|
||||
10
ansible/app-upgrade.yaml
Normal file
10
ansible/app-upgrade.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
- hosts: bots
|
||||
remote_user: root
|
||||
become: true
|
||||
tasks:
|
||||
- name: Upgrade Containers
|
||||
shell: |
|
||||
cd /app/bot
|
||||
docker-compose down
|
||||
docker-compose pull
|
||||
sudo -u keeper -g bot -- docker-compose up -d
|
||||
6
ansible/system-reboot.yaml
Normal file
6
ansible/system-reboot.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
- hosts: bots
|
||||
remote_user: root
|
||||
become: true
|
||||
tasks:
|
||||
- name: Reboot
|
||||
reboot:
|
||||
10
ansible/system-upgrade.yaml
Normal file
10
ansible/system-upgrade.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
- hosts: bots
|
||||
remote_user: root
|
||||
become: true
|
||||
tasks:
|
||||
- name: Upgrade System
|
||||
shell: |
|
||||
apt update
|
||||
apt upgrade -y
|
||||
- name: Reboot
|
||||
reboot:
|
||||
2
auto-swap/.gitignore
vendored
Normal file
2
auto-swap/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
node_modules/
|
||||
7
auto-swap/Dockerfile
Normal file
7
auto-swap/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
COPY src src
|
||||
CMD ["npm", "start"]
|
||||
18
auto-swap/README.md
Normal file
18
auto-swap/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Auto-Swap
|
||||
|
||||
## Configuration
|
||||
|
||||
- PRIVATE_KEY: Either use the PRIVATE_KEY or PRIVATE_KEY_FILE variable to set your Solana private key
|
||||
- SWAP_THRESHOLD: min amount of USDC in Drift account to initiate the swap. Default: 10
|
||||
- SWAP_RATION: ratio to which the swap should be executed. E.g. 0.5 = 50% of USDC will be swapped to SOL. Default: 0.5
|
||||
- AUTOSWA_INTERVAL: Time in ms of the interval to check if SWAP_THRESHOLD is reached. Default: 60000
|
||||
|
||||
Example: `.env` file:
|
||||
```
|
||||
RPC_ENDPOINT=https://your-rpc-endpoint
|
||||
PRIVATE_KEY="[123,456,...789]"
|
||||
#PRIVATE_KEY_FILE=~/.config/solana/bot.json
|
||||
SWAP_THRESHOLD=2
|
||||
SWAP_RATIO=0.6
|
||||
AUTOSWAP_INTERVAL=60000
|
||||
```
|
||||
1694
auto-swap/package-lock.json
generated
Normal file
1694
auto-swap/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
auto-swap/package.json
Normal file
18
auto-swap/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "auto-swap",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node --no-warnings=ExperimentalWarning src/main.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@drift-labs/sdk": "2.78.0-beta.0",
|
||||
"@solana/web3.js": "^1.89.1",
|
||||
"bigint-buffer": "^1.1.5",
|
||||
"dotenv": "^16.4.4"
|
||||
}
|
||||
}
|
||||
310
auto-swap/src/_main.js
Normal file
310
auto-swap/src/_main.js
Normal file
@@ -0,0 +1,310 @@
|
||||
require('dotenv').config()
|
||||
const web3 = require("@solana/web3.js");
|
||||
const drift = require("@drift-labs/sdk");
|
||||
|
||||
const LAMPORTS_PER_SOL = 1000000000;
|
||||
const AUTOSWAP_INTERVAL = process.env.AUTOSWAP_INTERVAL || 1000 * 60;
|
||||
|
||||
const USDC_MARKET = 0;
|
||||
const SOL_MARKET = 1;
|
||||
|
||||
const SWAP_RATIO = process.env.SWAP_RATIO || 0.5;
|
||||
const SWAP_THRESHOLD = process.env.SWAP_THRESHOLD || 10;
|
||||
|
||||
const USDC_INT = 1000000;
|
||||
|
||||
const SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';
|
||||
const USDC_MINT_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||
|
||||
const USDC_MINT_PUBLIC_KEY = new web3.PublicKey(USDC_MINT_ADDRESS);
|
||||
const keyPairFile = process.env.PRIVATE_KEY || process.env.PRIVATE_KEY_FILE;
|
||||
const wallet = new drift.Wallet(drift.loadKeypair(keyPairFile));
|
||||
const connection = new web3.Connection(process.env.RPC_ENDPOINT);
|
||||
|
||||
const thresholdReached = (amount) => amount / USDC_INT > SWAP_THRESHOLD;
|
||||
|
||||
const driftClient = new drift.DriftClient({
|
||||
connection,
|
||||
wallet,
|
||||
env: 'mainnet-beta',
|
||||
activeSubAccountId: 0,
|
||||
subAccountIds: [0],
|
||||
});
|
||||
|
||||
const sleep = (ms) => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const log = (msg) => {
|
||||
console.log(`[${new Date().toISOString()}] ${msg}`)
|
||||
};
|
||||
|
||||
const quoteNumber = (val) => {
|
||||
return drift.convertToNumber(val, drift.QUOTE_PRECISION);
|
||||
}
|
||||
|
||||
const baseNumber = (val) => {
|
||||
return drift.convertToNumber(val, drift.BASE_PRECISION);
|
||||
}
|
||||
|
||||
const quoteUsdcSol = async (amount) => {
|
||||
const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${USDC_MINT_ADDRESS}&outputMint=${SOL_MINT_ADDRESS}&amount=${amount}&slippageBps=50`;
|
||||
const quoteResponse = await (
|
||||
await fetch(quoteUrl)
|
||||
).json();
|
||||
return quoteResponse;
|
||||
}
|
||||
|
||||
const getWalletBalance = async (connection, publicKey) => {
|
||||
const lamportsBalance = await connection.getBalance(publicKey);
|
||||
return lamportsBalance / LAMPORTS_PER_SOL;
|
||||
};
|
||||
|
||||
const getUsdcBalance = async (connection, publicKey) => {
|
||||
const balance = await connection.getParsedTokenAccountsByOwner(
|
||||
publicKey, { mint: USDC_MINT_PUBLIC_KEY }
|
||||
);
|
||||
return balance.value[0]?.account.data.parsed.info.tokenAmount.uiAmount;
|
||||
};
|
||||
|
||||
const printInfo = async (user) => {
|
||||
let usdcInAccount = quoteNumber(user.getTokenAmount(USDC_MARKET));
|
||||
let solInWallet = await getWalletBalance(connection, wallet.publicKey);
|
||||
let usdcWallet = await getUsdcBalance(connection, wallet.publicKey)
|
||||
|
||||
log(`USDC in account: ${usdcInAccount}`);
|
||||
log(`USDC in wallet: ${usdcWallet}`);
|
||||
log(`SOL in wallet: ${solInWallet}`);
|
||||
};
|
||||
|
||||
const calcSwapAmount = (usdcInAccount) => {
|
||||
return usdcInAccount * SWAP_RATIO;
|
||||
};
|
||||
|
||||
const getSerializedTransaction = async (quote) => {
|
||||
return await (
|
||||
await fetch('https://quote-api.jup.ag/v6/swap', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
quoteResponse: quote,
|
||||
userPublicKey: wallet.publicKey.toString(),
|
||||
wrapAndUnwrapSol: true,
|
||||
dynamicComputeUnitLimit: true, // allow dynamic compute limit instead of max 1,400,000
|
||||
prioritizationFeeLamports: 'auto' // capped at 5,000,000 lamports / 0.005 SOL.
|
||||
})
|
||||
})
|
||||
).json();
|
||||
};
|
||||
|
||||
const signTransaction = (swapTransaction) => {
|
||||
const swapTransactionBuf = Buffer.from(swapTransaction, 'base64');
|
||||
var transaction = web3.VersionedTransaction.deserialize(swapTransactionBuf);
|
||||
transaction.sign([wallet.payer]);
|
||||
return transaction;
|
||||
};
|
||||
|
||||
const sendTransaction = async (transaction) => {
|
||||
const rawTransaction = transaction.serialize()
|
||||
return connection.sendRawTransaction(rawTransaction, {
|
||||
skipPreflight: false,
|
||||
preflightCommitment: 'confirmed',
|
||||
// Maximum number of times for the RPC node to retry sending the transaction to the leader.
|
||||
// If this parameter is not provided, the RPC node will retry the transaction until it is finalized or until the blockhash expires.
|
||||
//maxRetries: 0
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
const withdraw = async (marketIndex, withdrawAmount) => {
|
||||
const amount = driftClient.convertToSpotPrecision(marketIndex, withdrawAmount);
|
||||
const associatedTokenAccount = await driftClient.getAssociatedTokenAccount(marketIndex);
|
||||
const reduceOnly = true;
|
||||
return driftClient.withdraw(
|
||||
amount,
|
||||
marketIndex,
|
||||
associatedTokenAccount,
|
||||
reduceOnly
|
||||
);
|
||||
};
|
||||
|
||||
const signAndSendTx = async (quote) => {
|
||||
let tx = await getSerializedTransaction(quote);
|
||||
let signedTx = signTransaction(tx.swapTransaction);
|
||||
return sendTransaction(signedTx);
|
||||
};
|
||||
|
||||
const confirmTx = async (txSig) => {
|
||||
const latestBlockHash = await connection.getLatestBlockhash();
|
||||
return connection.confirmTransaction({
|
||||
blockhash: latestBlockHash.blockhash,
|
||||
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
|
||||
signature: txSig,
|
||||
}, 'confirmed');
|
||||
};
|
||||
|
||||
const run2 = async () => {
|
||||
|
||||
await driftClient.subscribe();
|
||||
const user = driftClient.getUser();
|
||||
|
||||
// force markets to be included
|
||||
driftClient.perpMarketLastSlotCache.set(SOL_MARKET, Number.MAX_SAFE_INTEGER);
|
||||
driftClient.perpMarketLastSlotCache.set(USDC_MARKET, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
log('---------------------------------')
|
||||
log("DriftClient initialized")
|
||||
log(`Swap Ratio: ${SWAP_RATIO}`);
|
||||
log(`Swap Threshold: ${SWAP_THRESHOLD} USDC`);
|
||||
printInfo(user);
|
||||
|
||||
let swapInProgress = false;
|
||||
|
||||
let lastUsdcInAccount = await connection.getBalance(publicKey);
|
||||
let lastSolInWallet = await getWalletBalance(connection, wallet.publicKey);
|
||||
|
||||
setInterval(async () => {
|
||||
if (!swapInProgress) {
|
||||
let currentUsdcInAccount = await connection.getBalance(publicKey);
|
||||
let currentSolInWallet = await getWalletBalance(connection, wallet.publicKey);
|
||||
|
||||
let differenceSol = lastSolInWallet - currentSolInWallet;
|
||||
let differenceUsdc = currentUsdcInAccount - lastUsdcInAccount;
|
||||
|
||||
await quoteUsdcSol(differenceUsdc)
|
||||
.then(quote => {
|
||||
log(`Swap: ${differenceUsdc / USDC_INT}$ to ${quote.outAmount / LAMPORTS_PER_SOL} SOL`);
|
||||
//return signAndSendTx(quote);
|
||||
})
|
||||
lastUsdcInAccount = currentUsdcInAccount;
|
||||
}
|
||||
}, AUTOSWAP_INTERVAL);
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
|
||||
await driftClient.subscribe();
|
||||
const user = driftClient.getUser();
|
||||
|
||||
// force markets to be included
|
||||
driftClient.perpMarketLastSlotCache.set(SOL_MARKET, Number.MAX_SAFE_INTEGER);
|
||||
driftClient.perpMarketLastSlotCache.set(USDC_MARKET, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
log('---------------------------------')
|
||||
log("DriftClient initialized")
|
||||
log(`Swap Ratio: ${SWAP_RATIO}`);
|
||||
log(`Swap Threshold: ${SWAP_THRESHOLD} USDC`);
|
||||
printInfo(user);
|
||||
|
||||
let swapInProgress = false;
|
||||
|
||||
setInterval(async () => {
|
||||
if (!swapInProgress) {
|
||||
try {
|
||||
let usdcInAccount = user.getTokenAmount(USDC_MARKET);
|
||||
let swapAmount = Math.floor(calcSwapAmount(usdcInAccount));
|
||||
|
||||
if (thresholdReached(usdcInAccount)) {
|
||||
log('---------------------------------')
|
||||
printInfo(user);
|
||||
swapInProgress = true;
|
||||
|
||||
let withdrawSuccessful = false;
|
||||
let withdrawRetries = 0;
|
||||
let maxWithdrawRetries = 3;
|
||||
while (!withdrawSuccessful && withdrawRetries < maxWithdrawRetries) {
|
||||
log(`Withdraw from exchange`);
|
||||
await withdraw(USDC_MARKET, quoteNumber(user.getTokenAmount(USDC_MARKET)))
|
||||
.then(async (txSig) => {
|
||||
log(`Confirm withdraw: https://solscan.io/tx/${txSig}`);
|
||||
return confirmTx(txSig);
|
||||
}, (error) => {
|
||||
throw `Withdraw TX failed: ${error}`;
|
||||
})
|
||||
.then((confirmResult) => {
|
||||
if (confirmResult.value.err) {
|
||||
throw `Withdraw not confirmed`;
|
||||
} else {
|
||||
log(`Withdraw confirmed`);
|
||||
withdrawSuccessful = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
withdrawRetries++;
|
||||
log(`Withdraw failed (${withdrawRetries}/${maxWithdrawRetries}): ${error}`);
|
||||
// TODO check if withdraw was still somehow successfull
|
||||
// it sometimes happens and we're then stuck in the loop
|
||||
});
|
||||
}
|
||||
|
||||
//backoff & try again in next iteration
|
||||
if (withdrawRetries === maxWithdrawRetries) {
|
||||
log('Withdraw failed - check if account balance changed and the withdraw actualy did not fail');
|
||||
let currentUsdcInAccount = user.getTokenAmount(USDC_MARKET);
|
||||
if(currentUsdcInAccount < usdcInAccount) {
|
||||
log('Withdraw seemed to went through - continue with swap');
|
||||
} else {
|
||||
log('Withdraw really failed - backoff & try again in next iteration');
|
||||
swapInProgress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let swapSuccessful = false
|
||||
let swapRetries = 0;
|
||||
let maxSwapRetries = 3;
|
||||
while (!swapSuccessful && swapRetries < maxSwapRetries) {
|
||||
// TODO calculate fresh swapAmount every iteration as collateral may has increased in the mean time
|
||||
await quoteUsdcSol(swapAmount)
|
||||
.then(quote => {
|
||||
log(`Swap: ${swapAmount / USDC_INT}$ to ${quote.outAmount / LAMPORTS_PER_SOL} SOL`);
|
||||
return signAndSendTx(quote);
|
||||
})
|
||||
.then(async txSig => {
|
||||
log(`Confirm swap: https://solscan.io/tx/${txSig}`);
|
||||
return confirmTx(txSig);
|
||||
})
|
||||
.then(confirmResult => {
|
||||
if (confirmResult.value.err) {
|
||||
throw `Confirm swap failed ${confirmResult.value.err}`;
|
||||
} else {
|
||||
log('Swap successful')
|
||||
log('---------------------------------')
|
||||
swapInProgress = false;
|
||||
swapSuccessful = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
swapRetries++;
|
||||
log(`FAAAAAIL (${swapRetries}/${maxSwapRetries}): ${error}`);
|
||||
})
|
||||
sleep(1000);
|
||||
}
|
||||
|
||||
//backoff & try again in next iteration
|
||||
if (swapRetries === maxSwapRetries) {
|
||||
log('Swap failed - backoff & try again in next iteration');
|
||||
swapInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
log(`Waiting to reach threshold - current balance on DEX: ${usdcInAccount / USDC_INT} USDC`);
|
||||
swapInProgress = false;
|
||||
}
|
||||
} catch (error) {
|
||||
log(error);
|
||||
swapInProgress = false;
|
||||
}
|
||||
} else {
|
||||
log('Swap in progress')
|
||||
}
|
||||
}, AUTOSWAP_INTERVAL);
|
||||
|
||||
};
|
||||
|
||||
run2();
|
||||
272
auto-swap/src/main.js
Normal file
272
auto-swap/src/main.js
Normal file
@@ -0,0 +1,272 @@
|
||||
require('dotenv').config()
|
||||
const web3 = require("@solana/web3.js");
|
||||
const drift = require("@drift-labs/sdk");
|
||||
|
||||
const LAMPORTS_PER_SOL = 1000000000;
|
||||
const AUTOSWAP_INTERVAL = process.env.AUTOSWAP_INTERVAL || 1000 * 60;
|
||||
|
||||
const USDC_MARKET = 0;
|
||||
const SOL_MARKET = 1;
|
||||
|
||||
const SWAP_RATIO = process.env.SWAP_RATIO || 0.5;
|
||||
const SWAP_THRESHOLD = process.env.SWAP_THRESHOLD || 10;
|
||||
|
||||
const USDC_INT = 1000000;
|
||||
|
||||
const SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';
|
||||
const USDC_MINT_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||
|
||||
const USDC_MINT_PUBLIC_KEY = new web3.PublicKey(USDC_MINT_ADDRESS);
|
||||
const keyPairFile = process.env.PRIVATE_KEY || process.env.PRIVATE_KEY_FILE;
|
||||
const wallet = new drift.Wallet(drift.loadKeypair(keyPairFile));
|
||||
const connection = new web3.Connection(process.env.RPC_ENDPOINT);
|
||||
|
||||
const thresholdReached = (amount) => amount / USDC_INT > SWAP_THRESHOLD;
|
||||
|
||||
const driftClient = new drift.DriftClient({
|
||||
connection,
|
||||
wallet,
|
||||
env: 'mainnet-beta',
|
||||
activeSubAccountId: 0,
|
||||
subAccountIds: [0],
|
||||
});
|
||||
|
||||
const sleep = (ms) => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const log = (msg) => {
|
||||
console.log(`[${new Date().toISOString()}] ${msg}`)
|
||||
};
|
||||
|
||||
const quoteNumber = (val) => {
|
||||
return drift.convertToNumber(val, drift.QUOTE_PRECISION);
|
||||
}
|
||||
|
||||
const baseNumber = (val) => {
|
||||
return drift.convertToNumber(val, drift.BASE_PRECISION);
|
||||
}
|
||||
|
||||
const quoteUsdcSol = async (amount) => {
|
||||
const quoteUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${USDC_MINT_ADDRESS}&outputMint=${SOL_MINT_ADDRESS}&amount=${amount}&slippageBps=50`;
|
||||
const quoteResponse = await (
|
||||
await fetch(quoteUrl)
|
||||
).json();
|
||||
return quoteResponse;
|
||||
}
|
||||
|
||||
const getWalletBalance = async (connection, publicKey) => {
|
||||
const lamportsBalance = await connection.getBalance(publicKey);
|
||||
return lamportsBalance / LAMPORTS_PER_SOL;
|
||||
};
|
||||
|
||||
const getUsdcBalance = async (connection, publicKey) => {
|
||||
const balance = await connection.getParsedTokenAccountsByOwner(
|
||||
publicKey, { mint: USDC_MINT_PUBLIC_KEY }
|
||||
);
|
||||
return balance.value[0]?.account.data.parsed.info.tokenAmount.uiAmount;
|
||||
};
|
||||
|
||||
const printInfo = async (user) => {
|
||||
let usdcInAccount = quoteNumber(user.getTokenAmount(USDC_MARKET));
|
||||
let solInWallet = await getWalletBalance(connection, wallet.publicKey);
|
||||
let usdcWallet = await getUsdcBalance(connection, wallet.publicKey)
|
||||
|
||||
log(`USDC in account: ${usdcInAccount}`);
|
||||
log(`USDC in wallet: ${usdcWallet}`);
|
||||
log(`SOL in wallet: ${solInWallet}`);
|
||||
};
|
||||
|
||||
const calcSwapAmount = (usdcInAccount) => {
|
||||
return usdcInAccount * SWAP_RATIO;
|
||||
};
|
||||
|
||||
const getSerializedTransaction = async (quote) => {
|
||||
return await (
|
||||
await fetch('https://quote-api.jup.ag/v6/swap', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
quoteResponse: quote,
|
||||
userPublicKey: wallet.publicKey.toString(),
|
||||
wrapAndUnwrapSol: true,
|
||||
dynamicComputeUnitLimit: true, // allow dynamic compute limit instead of max 1,400,000
|
||||
prioritizationFeeLamports: 'auto' // capped at 5,000,000 lamports / 0.005 SOL.
|
||||
})
|
||||
})
|
||||
).json();
|
||||
};
|
||||
|
||||
const signTransaction = (swapTransaction) => {
|
||||
const swapTransactionBuf = Buffer.from(swapTransaction, 'base64');
|
||||
var transaction = web3.VersionedTransaction.deserialize(swapTransactionBuf);
|
||||
transaction.sign([wallet.payer]);
|
||||
return transaction;
|
||||
};
|
||||
|
||||
const sendTransaction = async (transaction) => {
|
||||
const rawTransaction = transaction.serialize()
|
||||
return connection.sendRawTransaction(rawTransaction, {
|
||||
skipPreflight: false,
|
||||
preflightCommitment: 'confirmed',
|
||||
// Maximum number of times for the RPC node to retry sending the transaction to the leader.
|
||||
// If this parameter is not provided, the RPC node will retry the transaction until it is finalized or until the blockhash expires.
|
||||
//maxRetries: 0
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
const withdraw = async (marketIndex, withdrawAmount) => {
|
||||
const amount = driftClient.convertToSpotPrecision(marketIndex, withdrawAmount);
|
||||
const associatedTokenAccount = await driftClient.getAssociatedTokenAccount(marketIndex);
|
||||
const reduceOnly = true;
|
||||
return driftClient.withdraw(
|
||||
amount,
|
||||
marketIndex,
|
||||
associatedTokenAccount,
|
||||
reduceOnly
|
||||
);
|
||||
};
|
||||
|
||||
const signAndSendTx = async (quote) => {
|
||||
let tx = await getSerializedTransaction(quote);
|
||||
let signedTx = signTransaction(tx.swapTransaction);
|
||||
return sendTransaction(signedTx);
|
||||
};
|
||||
|
||||
const confirmTx = async (txSig) => {
|
||||
const latestBlockHash = await connection.getLatestBlockhash();
|
||||
return connection.confirmTransaction({
|
||||
blockhash: latestBlockHash.blockhash,
|
||||
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
|
||||
signature: txSig,
|
||||
}, 'confirmed');
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
|
||||
await driftClient.subscribe();
|
||||
const user = driftClient.getUser();
|
||||
|
||||
// force markets to be included
|
||||
driftClient.perpMarketLastSlotCache.set(SOL_MARKET, Number.MAX_SAFE_INTEGER);
|
||||
driftClient.perpMarketLastSlotCache.set(USDC_MARKET, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
log('---------------------------------')
|
||||
log("DriftClient initialized")
|
||||
log(`Swap Ratio: ${SWAP_RATIO}`);
|
||||
log(`Swap Threshold: ${SWAP_THRESHOLD} USDC`);
|
||||
printInfo(user);
|
||||
|
||||
let swapInProgress = false;
|
||||
|
||||
setInterval(async () => {
|
||||
if (!swapInProgress) {
|
||||
try {
|
||||
let usdcInAccount = user.getTokenAmount(USDC_MARKET);
|
||||
let swapAmount = Math.floor(calcSwapAmount(usdcInAccount));
|
||||
|
||||
if (thresholdReached(usdcInAccount)) {
|
||||
log('---------------------------------')
|
||||
printInfo(user);
|
||||
swapInProgress = true;
|
||||
|
||||
let withdrawSuccessful = false;
|
||||
let withdrawRetries = 0;
|
||||
let maxWithdrawRetries = 3;
|
||||
while (!withdrawSuccessful && withdrawRetries < maxWithdrawRetries) {
|
||||
log(`Withdraw from exchange`);
|
||||
await withdraw(USDC_MARKET, quoteNumber(user.getTokenAmount(USDC_MARKET)))
|
||||
.then(async (txSig) => {
|
||||
log(`Confirm withdraw: https://solscan.io/tx/${txSig}`);
|
||||
return confirmTx(txSig);
|
||||
}, (error) => {
|
||||
throw `Withdraw TX failed: ${error}`;
|
||||
})
|
||||
.then((confirmResult) => {
|
||||
if (confirmResult.value.err) {
|
||||
throw `Withdraw not confirmed`;
|
||||
} else {
|
||||
log(`Withdraw confirmed`);
|
||||
withdrawSuccessful = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
withdrawRetries++;
|
||||
log(`Withdraw failed (${withdrawRetries}/${maxWithdrawRetries}): ${error}`);
|
||||
// TODO check if withdraw was still somehow successfull
|
||||
// it sometimes happens and we're then stuck in the loop
|
||||
});
|
||||
}
|
||||
|
||||
//backoff & try again in next iteration
|
||||
if (withdrawRetries === maxWithdrawRetries) {
|
||||
log('Withdraw failed - check if account balance changed and the withdraw actualy did not fail');
|
||||
let currentUsdcInAccount = user.getTokenAmount(USDC_MARKET);
|
||||
if(currentUsdcInAccount < usdcInAccount) {
|
||||
log('Withdraw seemed to went through - continue with swap');
|
||||
} else {
|
||||
log('Withdraw really failed - backoff & try again in next iteration');
|
||||
swapInProgress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let swapSuccessful = false
|
||||
let swapRetries = 0;
|
||||
let maxSwapRetries = 3;
|
||||
while (!swapSuccessful && swapRetries < maxSwapRetries) {
|
||||
// TODO calculate fresh swapAmount every iteration as collateral may has increased in the mean time
|
||||
await quoteUsdcSol(swapAmount)
|
||||
.then(quote => {
|
||||
log(`Swap: ${swapAmount / USDC_INT}$ to ${quote.outAmount / LAMPORTS_PER_SOL} SOL`);
|
||||
return signAndSendTx(quote);
|
||||
})
|
||||
.then(async txSig => {
|
||||
log(`Confirm swap: https://solscan.io/tx/${txSig}`);
|
||||
return confirmTx(txSig);
|
||||
})
|
||||
.then(confirmResult => {
|
||||
if (confirmResult.value.err) {
|
||||
throw `Confirm swap failed ${confirmResult.value.err}`;
|
||||
} else {
|
||||
log('Swap successful')
|
||||
log('---------------------------------')
|
||||
swapInProgress = false;
|
||||
swapSuccessful = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
swapRetries++;
|
||||
log(`FAAAAAIL (${swapRetries}/${maxSwapRetries}): ${error}`);
|
||||
})
|
||||
sleep(1000);
|
||||
}
|
||||
|
||||
//backoff & try again in next iteration
|
||||
if (swapRetries === maxSwapRetries) {
|
||||
log('Swap failed - backoff & try again in next iteration');
|
||||
swapInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
log(`Waiting to reach threshold - current balance on DEX: ${usdcInAccount / USDC_INT} USDC`);
|
||||
swapInProgress = false;
|
||||
}
|
||||
} catch (error) {
|
||||
log(error);
|
||||
swapInProgress = false;
|
||||
}
|
||||
} else {
|
||||
log('Swap in progress')
|
||||
}
|
||||
}, AUTOSWAP_INTERVAL);
|
||||
|
||||
};
|
||||
|
||||
run();
|
||||
@@ -1,49 +0,0 @@
|
||||
#cloud-config
|
||||
groups:
|
||||
- ubuntu: [root,sys]
|
||||
- docker
|
||||
- bot
|
||||
|
||||
users:
|
||||
- default
|
||||
- name: keeper
|
||||
gecos: keeper
|
||||
shell: /bin/bash
|
||||
primary_group: bot
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
groups: users, admin, docker
|
||||
lock_passwd: false
|
||||
|
||||
packages:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg-agent
|
||||
- software-properties-common
|
||||
- chrony
|
||||
- docker.io
|
||||
- docker-compose
|
||||
|
||||
ntp:
|
||||
enabled: true
|
||||
ntp_client: chrony
|
||||
servers:
|
||||
- ${ntp_server}
|
||||
|
||||
runcmd:
|
||||
- git clone https://github.com/0x1d/drift-keeper /app/bot
|
||||
- mv /app/.env /app/bot/.env
|
||||
- mv /app/config.yaml /app/bot/config.yaml
|
||||
- cd /app/bot && sudo -u keeper -g bot -- docker-compose up -d
|
||||
|
||||
write_files:
|
||||
- path: /app/.env
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${env_file}
|
||||
- path: /app/config.yaml
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${config_file}
|
||||
71
cloud-init/cloud-config.yaml
Normal file
71
cloud-init/cloud-config.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
#cloud-config
|
||||
groups:
|
||||
- ubuntu: [root,sys]
|
||||
- docker
|
||||
- bot
|
||||
|
||||
users:
|
||||
- default
|
||||
- name: keeper
|
||||
gecos: keeper
|
||||
shell: /bin/bash
|
||||
primary_group: bot
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
groups: users, admin, docker
|
||||
lock_passwd: false
|
||||
|
||||
packages:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg-agent
|
||||
- software-properties-common
|
||||
- chrony
|
||||
- docker.io
|
||||
- docker-compose
|
||||
|
||||
ntp:
|
||||
enabled: true
|
||||
ntp_client: chrony
|
||||
servers:
|
||||
- ${ntp_server}
|
||||
|
||||
runcmd:
|
||||
- systemctl stop snapd && systemctl disable snapd
|
||||
- git clone -b feature/scaling https://github.com/0x1d/drift-keeper /app/bot
|
||||
- cp -rT /transfer /app/bot && rm -rf /transfer
|
||||
- chown -R keeper:bot /app/bot
|
||||
- cd /app/bot && sudo -u keeper -g bot -- docker-compose up -d
|
||||
|
||||
write_files:
|
||||
- path: /transfer/.env
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${env_file}
|
||||
- path: /transfer/.env.monitoring
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${env_monitoring_file}
|
||||
- path: /transfer/config.yaml
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${config_file}
|
||||
- path: /transfer/prometheus/prometheus.yml
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${prometheus_config_file}
|
||||
- path: /transfer/prometheus/web.yml
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${prometheus_web_file}
|
||||
- path: /transfer/docker-compose.yaml
|
||||
encoding: b64
|
||||
owner: root:root
|
||||
permissions: '0750'
|
||||
content: ${docker_compose_file}
|
||||
|
||||
@@ -56,7 +56,7 @@ global:
|
||||
eventSubscriberPollingInterval: 5000
|
||||
bulkAccountLoaderPollingInterval: 5000
|
||||
|
||||
useJito: false
|
||||
useJito: true
|
||||
jitoBlockEngineUrl:
|
||||
jitoAuthPrivateKey:
|
||||
|
||||
|
||||
149
ctl.sh
149
ctl.sh
@@ -5,38 +5,143 @@ API_ENDPOINT=https://api.mainnet-beta.solana.com/
|
||||
|
||||
source .env
|
||||
|
||||
function keeper {
|
||||
function build {
|
||||
##
|
||||
## Usage: ./ctl.sh COMMAND SUBCOMMAND
|
||||
##
|
||||
## ~> build
|
||||
## all Build all images
|
||||
## keeper Build bot image
|
||||
## tracker Build wallet-tracker image
|
||||
## autoswap Build auto-swap image
|
||||
##
|
||||
## ~> push
|
||||
## all Push all images to Docker registry
|
||||
## keeper Push bot image to Docker registry
|
||||
## tracker Push tracker image to Docker registry
|
||||
## autoswap Push auto-swaü image to Docker registry
|
||||
##
|
||||
## ~> run
|
||||
## all Run the complete stack locally
|
||||
## autoswap Run Auto-Swap locally
|
||||
##
|
||||
## ~> infra
|
||||
## plan Plan infrastructure change
|
||||
## provision Provision infrastructure
|
||||
## hosts Show list of servers
|
||||
## connect Connect to a server
|
||||
## playbook Run a maintenance playbook
|
||||
##
|
||||
## ~> balance
|
||||
## sol Show SOL balance
|
||||
## usdc Show USDC balance
|
||||
##
|
||||
|
||||
RED="31"
|
||||
GREEN="32"
|
||||
GREENBLD="\e[1;${GREEN}m"
|
||||
REDBOLD="\e[1;${RED}m"
|
||||
REDITALIC="\e[3;${RED}m"
|
||||
EC="\e[0m"
|
||||
|
||||
function info {
|
||||
printf "\n${GREENBLD}Wallet Address:\t$WALLET_ADDRESS${EC}\n"
|
||||
printf "${GREENBLD}Environment:\t$ENV${EC}\n"
|
||||
sed -n 's/^##//p' ctl.sh
|
||||
}
|
||||
|
||||
function build {
|
||||
function all {
|
||||
build keeper
|
||||
build tracker
|
||||
}
|
||||
function keeper {
|
||||
mkdir -p .build
|
||||
git clone https://github.com/drift-labs/keeper-bots-v2 -b mainnet-beta .build/keeper-bots-v2
|
||||
#pushd .build/keeper-bots-v2
|
||||
# git checkout 21fd791d142490fe033b5e25719927c106a0aaf2
|
||||
#popd
|
||||
docker build -f Dockerfile -t ${DOCKER_IMAGE} .build/keeper-bots-v2
|
||||
rm -rf .build
|
||||
}
|
||||
function push {
|
||||
docker push ${DOCKER_IMAGE}
|
||||
}
|
||||
${@:-}
|
||||
}
|
||||
|
||||
function tracker {
|
||||
function build {
|
||||
function tracker {
|
||||
pushd wallet-tracker
|
||||
docker build -t ${DOCKER_IMAGE_WALLET_TRACKER} .
|
||||
popd
|
||||
}
|
||||
function push {
|
||||
docker push ${DOCKER_IMAGE_WALLET_TRACKER}
|
||||
function autoswap {
|
||||
pushd auto-swap
|
||||
docker build -t ${DOCKER_IMAGE_AUTO_SWAP} .
|
||||
popd
|
||||
}
|
||||
function metrics {
|
||||
pushd user-metrics
|
||||
docker build -t ${DOCKER_IMAGE_USER_METRICS} .
|
||||
popd
|
||||
}
|
||||
${@:-}
|
||||
}
|
||||
|
||||
function droplet {
|
||||
function push {
|
||||
function all {
|
||||
push keeper
|
||||
push tracker
|
||||
}
|
||||
function keeper {
|
||||
docker push ${DOCKER_IMAGE}
|
||||
}
|
||||
function tracker {
|
||||
docker push ${DOCKER_IMAGE_WALLET_TRACKER}
|
||||
}
|
||||
function autoswap {
|
||||
docker push ${DOCKER_IMAGE_AUTO_SWAP}
|
||||
}
|
||||
function metrics {
|
||||
docker push ${DOCKER_IMAGE_USER_METRICS}
|
||||
}
|
||||
${@:-}
|
||||
}
|
||||
|
||||
function run {
|
||||
function all {
|
||||
docker compose up
|
||||
}
|
||||
function autoswap {
|
||||
pushd auto-swap
|
||||
npm start
|
||||
popd
|
||||
}
|
||||
function metircs {
|
||||
pushd user-metrics
|
||||
npm start
|
||||
popd
|
||||
}
|
||||
${@:-}
|
||||
}
|
||||
|
||||
function infra {
|
||||
function plan {
|
||||
terraform init
|
||||
terraform plan
|
||||
}
|
||||
function provision {
|
||||
terraform init
|
||||
terraform apply
|
||||
echo "[bots]" > inventory.cfg
|
||||
terraform output -json | jq --raw-output ' .instances.value | to_entries[] | .value' >> inventory.cfg
|
||||
}
|
||||
function hosts {
|
||||
terraform output -json | jq --raw-output '.instances.value | to_entries[] | [.key, .value] | @tsv'
|
||||
}
|
||||
function connect {
|
||||
ssh root@$(terraform output -raw droplet_ip)
|
||||
infra hosts \
|
||||
| fzf --height=~50 \
|
||||
| awk '{print $2}' \
|
||||
| xargs -o ssh -l root $@
|
||||
}
|
||||
function playbook {
|
||||
pushd ansible
|
||||
ansible-playbook --ssh-common-args='-o StrictHostKeyChecking=accept-new' -i ../inventory.cfg $(fzf --height=~10)
|
||||
popd
|
||||
}
|
||||
${@:-}
|
||||
}
|
||||
@@ -77,4 +182,18 @@ function balance {
|
||||
${@:-}
|
||||
}
|
||||
|
||||
${@:-}
|
||||
function repl {
|
||||
clear
|
||||
cat motd
|
||||
info
|
||||
echo -e "\n${REDBOLD}Enter command...${EC}"
|
||||
read -p '~> ';
|
||||
clear
|
||||
cat motd
|
||||
./ctl.sh ${REPLY}
|
||||
printf "\n"
|
||||
read -p "Press any key to continue."
|
||||
repl
|
||||
}
|
||||
|
||||
${@:-info}
|
||||
@@ -2,25 +2,29 @@ version: '3'
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Bot
|
||||
# ---------------------------------------------------
|
||||
keeper:
|
||||
image: ${DOCKER_IMAGE}
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./config.yaml:/app/config.yaml
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Monitoring
|
||||
# ---------------------------------------------------
|
||||
auto-swap:
|
||||
image: ${DOCKER_IMAGE_AUTO_SWAP}
|
||||
build:
|
||||
context: auto-swap
|
||||
env_file: .env.autoswap
|
||||
restart: unless-stopped
|
||||
wallet-tracker:
|
||||
image: ${DOCKER_IMAGE_WALLET_TRACKER}
|
||||
build:
|
||||
context: wallet-tracker
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
user-metrics:
|
||||
image: wirelos/user-metrics:0.1.0
|
||||
env_file: .env.user-metrics
|
||||
restart: unless-stopped
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
container_name: prometheus
|
||||
@@ -51,12 +55,10 @@ services:
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
container_name: grafana
|
||||
env_file: .env.monitoring
|
||||
ports:
|
||||
- 3000:3000
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Build Settings
|
||||
DOCKER_IMAGE=wirelos/drift-keeper:mainnet-beta
|
||||
DOCKER_IMAGE_AUTO_SWAP=wirelos/auto-swap:0.1.0
|
||||
DOCKER_IMAGE_WALLET_TRACKER=wirelos/solana-wallet-tracker:0.1.0
|
||||
|
||||
# Shell Utils
|
||||
WALLET_ADDRESS=h5XjtA.....
|
||||
|
||||
# Grafana Settings
|
||||
GRAFANA_ADMIN_PASSWORD=grafana
|
||||
|
||||
# Drift Config
|
||||
ENV=mainnet-beta
|
||||
KEEPER_PRIVATE_KEY="[123,345,...]"
|
||||
@@ -24,7 +23,6 @@ ENDPOINT=http://178.63.126.77:8899
|
||||
# Teraswitch2 US
|
||||
#ENDPOINT=http://74.118.139.68:8899
|
||||
|
||||
|
||||
# ExtrNode
|
||||
#ENDPOINT=https://solana-mainnet.rpc.extrnode.com/your-api-key
|
||||
#WS_ENDPOINT=wss://solana-mainnet.rpc.extrnode.com/your-api-key
|
||||
|
||||
6
example.env.autoswap
Normal file
6
example.env.autoswap
Normal file
@@ -0,0 +1,6 @@
|
||||
RPC_ENDPOINT=https://your-rpc-endpoint
|
||||
PRIVATE_KEY="[123,456,...789]"
|
||||
#PRIVATE_KEY_FILE=~/.config/solana/bot.json
|
||||
SWAP_THRESHOLD=2
|
||||
SWAP_RATIO=0.6
|
||||
AUTOSWAP_INTERVAL=60000
|
||||
3
example.env.monitoring
Normal file
3
example.env.monitoring
Normal file
@@ -0,0 +1,3 @@
|
||||
GF_SECURITY_ADMIN_USER=admin
|
||||
GF_SECURITY_ADMIN_PASSWORD=grafana
|
||||
PROMETHEUS_PASSWORD=prompass
|
||||
3
example.env.user-metrics
Normal file
3
example.env.user-metrics
Normal file
@@ -0,0 +1,3 @@
|
||||
RPC_ENDPOINT=https://your-rpc-endpoint
|
||||
PRIVATE_KEY="[123,456,...789]"
|
||||
#PRIVATE_KEY_FILE=~/.config/solana/bot.json
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,4 +10,4 @@ datasources:
|
||||
basicAuth: true
|
||||
basicAuthUser: prom
|
||||
secureJsonData:
|
||||
basicAuthPassword: prompass
|
||||
basicAuthPassword: ${PROMETHEUS_PASSWORD}
|
||||
119
main.tf
119
main.tf
@@ -1,5 +1,9 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
linode = {
|
||||
source = "linode/linode"
|
||||
version = "2.13.0"
|
||||
}
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = "~> 2.0"
|
||||
@@ -7,41 +11,110 @@ terraform {
|
||||
}
|
||||
}
|
||||
|
||||
variable "do_token" {}
|
||||
variable "config" {
|
||||
default = {
|
||||
region = "ams3"
|
||||
ntp_server = "ntp.amsterdam.jito.wtf"
|
||||
docker_image = "wirelos/drift-keeper:mainnet-beta"
|
||||
}
|
||||
}
|
||||
|
||||
provider "digitalocean" {
|
||||
token = var.do_token
|
||||
}
|
||||
|
||||
provider "linode" {
|
||||
token = var.linode_token
|
||||
}
|
||||
|
||||
locals {
|
||||
user_data = templatefile("cloud-config.yaml", {
|
||||
ntp_server = var.config.ntp_server
|
||||
env_file = base64encode(file(".env"))
|
||||
config_file = base64encode(file("config.yaml"))
|
||||
})
|
||||
monitoring_config = {
|
||||
env = base64encode(templatefile("templates/monitoring/env.monitoring.tpl", var.monitoring))
|
||||
prometheus = base64encode(templatefile("templates/monitoring/prometheus/prometheus.yml.tpl", {
|
||||
wallet_address = var.bot.wallet_address
|
||||
}))
|
||||
prometheus_web = base64encode(templatefile("templates/monitoring/prometheus/web.yml.tpl", {
|
||||
prometheus_password_bcrypt = bcrypt(var.monitoring.prometheus_password)
|
||||
}))
|
||||
}
|
||||
cloud_config = { for s in concat(var.linode_instances, var.digitalocean_instances) : s.label => templatefile("cloud-init/cloud-config.yaml", {
|
||||
ntp_server = s.ntp_server
|
||||
env_file = base64encode(templatefile("templates/bot/env.tpl", merge(var.bot, {
|
||||
jito_block_engine_url = s.jito_block_engine_url
|
||||
})))
|
||||
config_file = base64encode(templatefile("templates/bot/config.yaml.tpl", {
|
||||
use_jito = s.use_jito
|
||||
}))
|
||||
env_monitoring_file = local.monitoring_config.env
|
||||
prometheus_config_file = local.monitoring_config.prometheus
|
||||
prometheus_web_file = local.monitoring_config.prometheus_web
|
||||
docker_compose_file = base64encode(templatefile("templates/bot/docker-compose.yaml.tpl", {
|
||||
docker_image = var.bot.docker_image
|
||||
docker_image_wallet_tracker = var.bot.docker_image_wallet_tracker
|
||||
}))
|
||||
}) }
|
||||
}
|
||||
|
||||
resource "linode_sshkey" "master" {
|
||||
label = "master-key"
|
||||
ssh_key = chomp(file("~/.ssh/id_rsa.pub"))
|
||||
}
|
||||
|
||||
resource "digitalocean_ssh_key" "default" {
|
||||
name = "Keeper Key"
|
||||
public_key = file("~/.ssh/id_rsa.pub")
|
||||
name = "master-key"
|
||||
public_key = chomp(file("~/.ssh/id_rsa.pub"))
|
||||
}
|
||||
|
||||
resource "linode_instance" "keeper" {
|
||||
for_each = { for s in var.linode_instances : s.label => s }
|
||||
label = each.key
|
||||
image = each.value.image
|
||||
group = each.value.group
|
||||
region = each.value.region
|
||||
type = each.value.type
|
||||
authorized_keys = [linode_sshkey.master.ssh_key]
|
||||
metadata {
|
||||
user_data = base64encode(local.cloud_config[each.key])
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
metadata
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "digitalocean_droplet" "keeper" {
|
||||
image = "ubuntu-23-10-x64"
|
||||
name = "drift-keeper"
|
||||
region = var.config.region
|
||||
size = "s-1vcpu-1gb-intel"
|
||||
for_each = { for s in var.digitalocean_instances : s.label => s }
|
||||
image = each.value.image
|
||||
name = each.key
|
||||
region = each.value.region
|
||||
size = each.value.type
|
||||
ssh_keys = [digitalocean_ssh_key.default.fingerprint]
|
||||
user_data = local.user_data
|
||||
user_data = local.cloud_config[each.key]
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
user_data
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
output "droplet_ip" {
|
||||
value = digitalocean_droplet.keeper.ipv4_address
|
||||
output "instances" {
|
||||
value = merge(
|
||||
tomap({ for k, v in linode_instance.keeper : k => v.ip_address }),
|
||||
tomap({ for k, v in digitalocean_droplet.keeper : k => v.ipv4_address })
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
output "panopticonf" {
|
||||
value = templatefile("templates/monitoring/prometheus/panopticon.yml.tpl", {
|
||||
user = "prom"
|
||||
password = var.monitoring.prometheus_password
|
||||
targets = <<-EOT
|
||||
%{for k, v in merge(
|
||||
tomap({ for k, v in linode_instance.keeper : k => v.ip_address }),
|
||||
tomap({ for k, v in digitalocean_droplet.keeper : k => v.ipv4_address })
|
||||
)}
|
||||
- targets: ['${v}:9090']
|
||||
labels:
|
||||
server: ${k}
|
||||
%{endfor}
|
||||
EOT
|
||||
})
|
||||
}
|
||||
|
||||
output "configurations" {
|
||||
value = local.cloud_config
|
||||
}
|
||||
|
||||
11
motd
Normal file
11
motd
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
▓█████▄ ██▀███ ██▓ █████▒▄▄▄█████▓ ██ ▄█▀▓█████ ▓█████ ██▓███ ▓█████ ██▀███
|
||||
▒██▀ ██▌▓██ ▒ ██▒▓██▒▓██ ▒ ▓ ██▒ ▓▒ ██▄█▒ ▓█ ▀ ▓█ ▀ ▓██░ ██▒▓█ ▀ ▓██ ▒ ██▒
|
||||
░██ █▌▓██ ░▄█ ▒▒██▒▒████ ░ ▒ ▓██░ ▒░ ▓███▄░ ▒███ ▒███ ▓██░ ██▓▒▒███ ▓██ ░▄█ ▒
|
||||
░▓█▄ ▌▒██▀▀█▄ ░██░░▓█▒ ░ ░ ▓██▓ ░ ▓██ █▄ ▒▓█ ▄ ▒▓█ ▄ ▒██▄█▓▒ ▒▒▓█ ▄ ▒██▀▀█▄
|
||||
░▒████▓ ░██▓ ▒██▒░██░░▒█░ ▒██▒ ░ ▒██▒ █▄░▒████▒░▒████▒▒██▒ ░ ░░▒████▒░██▓ ▒██▒
|
||||
▒▒▓ ▒ ░ ▒▓ ░▒▓░░▓ ▒ ░ ▒ ░░ ▒ ▒▒ ▓▒░░ ▒░ ░░░ ▒░ ░▒▓▒░ ░ ░░░ ▒░ ░░ ▒▓ ░▒▓░
|
||||
░ ▒ ▒ ░▒ ░ ▒░ ▒ ░ ░ ░ ░ ░▒ ▒░ ░ ░ ░ ░ ░ ░░▒ ░ ░ ░ ░ ░▒ ░ ▒░
|
||||
░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░░ ░ ░ ░ ░░ ░ ░░ ░
|
||||
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
||||
░
|
||||
27
panopticon/docker-compose.yaml
Normal file
27
panopticon/docker-compose.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--web.config.file=/etc/prometheus/web.yml'
|
||||
ports:
|
||||
- 9090:9090
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./prometheus:/etc/prometheus
|
||||
- prom_data:/prometheus
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
env_file: .env.monitoring
|
||||
ports:
|
||||
- 3000:3000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards
|
||||
|
||||
volumes:
|
||||
prom_data:
|
||||
3
panopticon/example.env.monitoring
Normal file
3
panopticon/example.env.monitoring
Normal file
@@ -0,0 +1,3 @@
|
||||
GF_SECURITY_ADMIN_USER=admin
|
||||
GF_SECURITY_ADMIN_PASSWORD=grafana
|
||||
PROMETHEUS_PASSWORD=prompass
|
||||
1877
panopticon/grafana/dashboards/drift-keeper.json
Normal file
1877
panopticon/grafana/dashboards/drift-keeper.json
Normal file
File diff suppressed because it is too large
Load Diff
1011
panopticon/grafana/dashboards/node-resources.json
Normal file
1011
panopticon/grafana/dashboards/node-resources.json
Normal file
File diff suppressed because it is too large
Load Diff
24
panopticon/grafana/provisioning/dashboards/providers.yml
Normal file
24
panopticon/grafana/provisioning/dashboards/providers.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
# <string> an unique provider name. Required
|
||||
- name: 'a unique provider name'
|
||||
# <int> Org id. Default to 1
|
||||
orgId: 1
|
||||
# <string> name of the dashboard folder.
|
||||
folder: ''
|
||||
# <string> folder UID. will be automatically generated if not specified
|
||||
folderUid: ''
|
||||
# <string> provider type. Default to 'file'
|
||||
type: file
|
||||
# <bool> disable dashboard deletion
|
||||
disableDeletion: false
|
||||
# <int> how often Grafana will scan for changed dashboards
|
||||
updateIntervalSeconds: 10
|
||||
# <bool> allow updating provisioned dashboards from the UI
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
# <string, required> path to dashboard files on disk. Required when using the 'file' type
|
||||
path: /var/lib/grafana/dashboards
|
||||
# <bool> use folder names from filesystem to create folders in Grafana
|
||||
foldersFromFilesStructure: true
|
||||
13
panopticon/grafana/provisioning/datasources/datasource.yml
Normal file
13
panopticon/grafana/provisioning/datasources/datasource.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
access: proxy
|
||||
editable: true
|
||||
basicAuth: true
|
||||
basicAuthUser: prom
|
||||
secureJsonData:
|
||||
basicAuthPassword: ${PROMETHEUS_PASSWORD}
|
||||
29
panopticon/prometheus/prometheus.yml
Normal file
29
panopticon/prometheus/prometheus.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
global:
|
||||
scrape_interval: 60s
|
||||
scrape_timeout: 10s
|
||||
evaluation_interval: 15s
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets: []
|
||||
scheme: http
|
||||
timeout: 10s
|
||||
api_version: v1
|
||||
scrape_configs:
|
||||
- job_name: 'federate'
|
||||
scrape_interval: 60s
|
||||
|
||||
honor_labels: true
|
||||
metrics_path: '/federate'
|
||||
|
||||
basic_auth:
|
||||
username: '${user}'
|
||||
password: '${password}'
|
||||
|
||||
params:
|
||||
'match[]':
|
||||
- '{instance="node-exporter:9100"}'
|
||||
- '{__name__=~"up:.*"}'
|
||||
|
||||
static_configs:
|
||||
${targets}
|
||||
4
panopticon/prometheus/web.yml
Normal file
4
panopticon/prometheus/web.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
basic_auth_users:
|
||||
# bcrypt "prompass"
|
||||
# e.g. htpasswd -bnBC 10 "" prompass | tr -d ':\n'
|
||||
prom: $2y$10$oWtHm79bh0D1CnNC4brGiOU7y6MbYa6cgklF/g6ek9YZYkgXfeOIu
|
||||
@@ -19,6 +19,12 @@ scrape_configs:
|
||||
static_configs:
|
||||
- targets:
|
||||
- wallet-tracker:3000
|
||||
- job_name: user
|
||||
scrape_interval: 60s
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- user-metrics:3000
|
||||
- job_name: keeper
|
||||
honor_timestamps: true
|
||||
scrape_interval: 15s
|
||||
|
||||
11
shell.nix
Normal file
11
shell.nix
Normal file
@@ -0,0 +1,11 @@
|
||||
let
|
||||
unstable = import (fetchTarball https://nixos.org/channels/nixos-unstable/nixexprs.tar.xz) { };
|
||||
in
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
ansible
|
||||
terraform
|
||||
solana-cli
|
||||
];
|
||||
}
|
||||
233
templates/bot/config.yaml.tpl
Normal file
233
templates/bot/config.yaml.tpl
Normal file
@@ -0,0 +1,233 @@
|
||||
global:
|
||||
# devnet or mainnet-beta
|
||||
driftEnv: mainnet-beta
|
||||
|
||||
# RPC endpoint to use
|
||||
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
|
||||
|
||||
# skips preflight checks on sendTransaciton, default is false.
|
||||
# this will speed up tx sending, but may increase SOL paid due to failed txs landing
|
||||
# on chain
|
||||
txSkipPreflight: false
|
||||
|
||||
# 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:
|
||||
|
||||
initUser: false # initialize user on startup
|
||||
testLiveness: false # test liveness, by failing liveness test after 1 min
|
||||
|
||||
# Force deposit this amount of USDC to collateral account, the program will
|
||||
# end after the deposit transaction is sent
|
||||
#forceDeposit: 1000
|
||||
|
||||
websocket: false # use websocket for account loading and events (limited support)
|
||||
eventSubscriber: false # disables event subscriber (heavy RPC demand), this is primary used for counting fills
|
||||
runOnce: false # Set true to run once and exit, useful for testing or one off bot runs
|
||||
debug: false # Enable debug logging
|
||||
txSenderType: "fast"
|
||||
|
||||
# subaccountIDs to load, if null will load subaccount 0 (default).
|
||||
# Even if bot specific configs requires subaccountIDs, you should still
|
||||
# specify it here, since we load the subaccounts before loading individual
|
||||
# bots.
|
||||
# subaccounts:
|
||||
# - 0
|
||||
# - 1
|
||||
# - 2
|
||||
|
||||
eventSubscriberPollingInterval: 5000
|
||||
bulkAccountLoaderPollingInterval: 5000
|
||||
|
||||
useJito: ${use_jito}
|
||||
# one of: ['non-jito-only', 'jito-only', 'hybrid'].
|
||||
# * non-jito-only: will only send txs to RPC when there is no active jito leader
|
||||
# * jito-only: will only send txs via bundle when there is an active jito leader
|
||||
# * hybrid: will attempt to send bundles when active jito leader, and use RPC when not
|
||||
# hybrid may not work well if using high throughput bots such as a filler depending on infra limitations.
|
||||
jitoStrategy: jito-only
|
||||
# the minimum tip to pay
|
||||
jitoMinBundleTip: 10000
|
||||
# the maximum tip to pay (will pay this once jitoMaxBundleFailCount is hit)
|
||||
jitoMaxBundleTip: 100000
|
||||
# the number of failed bundles (accepted but not landed) before tipping the max tip
|
||||
jitoMaxBundleFailCount: 200
|
||||
# the tip multiplier to use when tipping the max tip
|
||||
# controls superlinearity (1 = linear, 2 = slightly-superlinear, 3 = more-superlinear, ...)
|
||||
jitoTipMultiplier: 3
|
||||
jitoBlockEngineUrl:
|
||||
jitoAuthPrivateKey:
|
||||
|
||||
# which subaccounts to load, if null will load subaccount 0 (default)
|
||||
subaccounts:
|
||||
|
||||
# perpMarketIndexes:
|
||||
# - 1
|
||||
|
||||
# Which bots to run, be careful with this, running multiple bots in one instance
|
||||
# might use more resources than expected.
|
||||
# Bot specific configs are below
|
||||
enabledBots:
|
||||
# Perp order filler bot
|
||||
- fillerLite
|
||||
|
||||
# Spot order filler bot
|
||||
#- spotFiller
|
||||
|
||||
# Trigger bot (triggers trigger orders)
|
||||
#- trigger
|
||||
|
||||
# Liquidator bot, liquidates unhealthy positions by taking over the risk (caution, you should manage risk here)
|
||||
# - liquidator
|
||||
|
||||
# Example maker bot that participates in JIT auction (caution: you will probably lose money)
|
||||
# - jitMaker
|
||||
|
||||
# Example maker bot that posts floating oracle orders
|
||||
#- floatingMaker
|
||||
|
||||
# settles PnLs for the insurance fund (may want to run with runOnce: true)
|
||||
# - ifRevenueSettler
|
||||
|
||||
# settles negative PnLs for users (may want to run with runOnce: true)
|
||||
# - userPnlSettler
|
||||
|
||||
# - markTwapCrank
|
||||
|
||||
# below are bot configs
|
||||
botConfigs:
|
||||
|
||||
fillerLite:
|
||||
botId: "fillerLite"
|
||||
dryRun: false
|
||||
fillerPollingInterval: 6000
|
||||
metricsPort: 9464
|
||||
revertOnFailure: true
|
||||
simulateTxForCUEstimate: true
|
||||
|
||||
filler:
|
||||
botId: "filler"
|
||||
dryRun: false
|
||||
fillerPollingInterval: 6000
|
||||
metricsPort: 9464
|
||||
|
||||
# will revert a transaction during simulation if a fill fails, this will save on tx fees,
|
||||
# and be friendlier for use with services like Jito.
|
||||
# Default is true
|
||||
revertOnFailure: true
|
||||
|
||||
# calls simulateTransaction before sending to get an accurate CU estimate
|
||||
# as well as stop before sending the transaction (Default is true)
|
||||
simulateTxForCUEstimate: true
|
||||
|
||||
spotFiller:
|
||||
botId: "spot-filler"
|
||||
dryRun: false
|
||||
fillerPollingInterval: 6000
|
||||
metricsPort: 9465
|
||||
revertOnFailure: true
|
||||
simulateTxForCUEstimate: true
|
||||
|
||||
liquidator:
|
||||
botId: "liquidator"
|
||||
dryRun: false
|
||||
metricsPort: 9466
|
||||
# if true will NOT attempt to sell off any inherited positions
|
||||
disableAutoDerisking: false
|
||||
# if true will swap spot assets on jupiter if the price is better
|
||||
useJupiter: true
|
||||
# null will handle all markets
|
||||
perpMarketIndicies:
|
||||
spotMarketIndicies:
|
||||
|
||||
# this replaces perpMarketIndicies and spotMarketIndicies by allowing you to specify
|
||||
# which subaccount is responsible for liquidating markets
|
||||
# Markets are defined on perpMarkets.ts and spotMarkets.ts on the protocol codebase
|
||||
# Note: you must set global.subaccounts with all the subaccounts you want to load
|
||||
perpSubAccountConfig:
|
||||
0:
|
||||
# subaccount 0 will watch perp markets 0 and 1
|
||||
- 0
|
||||
- 1
|
||||
spotSubAccountConfig:
|
||||
0:
|
||||
# subaccount 0 will watch all spot markets
|
||||
|
||||
# max slippage (from oracle price) to incur allow when derisking
|
||||
maxSlippagePct: 0.05
|
||||
|
||||
# what algo to use for derisking. Options are "market" or "twap"
|
||||
deriskAlgo: "market"
|
||||
|
||||
# if deriskAlgo == "twap", must supply these as well
|
||||
# twapDurationSec: 300 # overall duration of to run the twap algo. Aims to derisk the entire position over this duration
|
||||
|
||||
# Minimum deposit amount to try to liquidiate, per spot market, in lamports.
|
||||
# If null, or a spot market isn't here, it will liquidate any amount
|
||||
# See perpMarkets.ts on the protocol codebase for the indices
|
||||
minDepositToLiq:
|
||||
1: 10
|
||||
2: 1000
|
||||
|
||||
# Filter out un-liquidateable accounts that just create log noise
|
||||
excludedAccounts:
|
||||
- 9CJLgd5f9nmTp7KRV37RFcQrfEmJn6TU87N7VQAe2Pcq
|
||||
- Edh39zr8GnQFNYwyvxhPngTJHrr29H3vVup8e8ZD4Hwu
|
||||
|
||||
# max % of collateral to spend when liquidating a user. In percentage terms (0.5 = 50%)
|
||||
maxPositionTakeoverPctOfCollateral: 0.5
|
||||
|
||||
# sends a webhook notification (slack, discord, etc.) when a liquidation is attempted (can be noisy due to partial liquidations)
|
||||
notifyOnLiquidation: true
|
||||
|
||||
trigger:
|
||||
botId: "trigger"
|
||||
dryRun: false
|
||||
metricsPort: 9465
|
||||
|
||||
markTwapCrank:
|
||||
botId: "mark-twap-cranker"
|
||||
dryRun: false
|
||||
metricsPort: 9465
|
||||
crankIntervalToMarketIndicies:
|
||||
15000:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
60000:
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 10
|
||||
- 11
|
||||
- 12
|
||||
- 13
|
||||
- 14
|
||||
- 15
|
||||
- 16
|
||||
60
templates/bot/docker-compose-aio.yaml.tpl
Normal file
60
templates/bot/docker-compose-aio.yaml.tpl
Normal file
@@ -0,0 +1,60 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
keeper:
|
||||
image: ${docker_image}
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./config.yaml:/app/config.yaml
|
||||
wallet-tracker:
|
||||
image: ${docker_image_wallet_tracker}
|
||||
build:
|
||||
context: wallet-tracker
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
user-metrics:
|
||||
image: wirelos/user-metrics:0.1.0
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
container_name: prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--web.config.file=/etc/prometheus/web.yml'
|
||||
ports:
|
||||
- 9090:9090
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./prometheus:/etc/prometheus
|
||||
- prom_data:/prometheus
|
||||
node-exporter:
|
||||
image: prom/node-exporter:latest
|
||||
container_name: node-exporter
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- '--path.procfs=/host/proc'
|
||||
- '--path.rootfs=/rootfs'
|
||||
- '--path.sysfs=/host/sys'
|
||||
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
|
||||
expose:
|
||||
- 9100
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
container_name: grafana
|
||||
env_file: .env.monitoring
|
||||
ports:
|
||||
- 3000:3000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards
|
||||
|
||||
volumes:
|
||||
prom_data:
|
||||
9
templates/bot/docker-compose.yaml.tpl
Normal file
9
templates/bot/docker-compose.yaml.tpl
Normal file
@@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
keeper:
|
||||
image: ${docker_image}
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./config.yaml:/app/config.yaml
|
||||
13
templates/bot/env.tpl
Normal file
13
templates/bot/env.tpl
Normal file
@@ -0,0 +1,13 @@
|
||||
DOCKER_IMAGE=wirelos/drift-keeper:mainnet-beta
|
||||
DOCKER_IMAGE_WALLET_TRACKER=wirelos/solana-wallet-tracker:latest
|
||||
|
||||
ENV=mainnet-beta
|
||||
ENDPOINT=${rpc_endpoint}
|
||||
RPC_ENDPOINT=${rpc_endpoint}
|
||||
WS_ENDPOINT=${ws_endpoint}
|
||||
|
||||
WALLET_ADDRESS=${wallet_address}
|
||||
KEEPER_PRIVATE_KEY="${keeper_private_key}"
|
||||
PRIVATE_KEY="${keeper_private_key}"
|
||||
JITO_BLOCK_ENGINE_URL=${jito_block_engine_url}
|
||||
JITO_AUTH_PRIVATE_KEY="${jito_private_key}"
|
||||
3
templates/monitoring/env.monitoring.tpl
Normal file
3
templates/monitoring/env.monitoring.tpl
Normal file
@@ -0,0 +1,3 @@
|
||||
GF_SECURITY_ADMIN_USER=${grafana_user}
|
||||
GF_SECURITY_ADMIN_PASSWORD=${grafana_password}
|
||||
PROMETHEUS_PASSWORD=${prometheus_password}
|
||||
29
templates/monitoring/prometheus/panopticon.yml.tpl
Normal file
29
templates/monitoring/prometheus/panopticon.yml.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
global:
|
||||
scrape_interval: 60s
|
||||
scrape_timeout: 10s
|
||||
evaluation_interval: 15s
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets: []
|
||||
scheme: http
|
||||
timeout: 10s
|
||||
api_version: v1
|
||||
scrape_configs:
|
||||
- job_name: 'federate'
|
||||
scrape_interval: 60s
|
||||
|
||||
honor_labels: true
|
||||
metrics_path: '/federate'
|
||||
|
||||
basic_auth:
|
||||
username: '${user}'
|
||||
password: '${password}'
|
||||
|
||||
params:
|
||||
'match[]':
|
||||
- '{instance="node-exporter:9100"}'
|
||||
- '{__name__=~"up:.*"}'
|
||||
|
||||
static_configs:
|
||||
${targets}
|
||||
42
templates/monitoring/prometheus/prometheus.yml.tpl
Normal file
42
templates/monitoring/prometheus/prometheus.yml.tpl
Normal file
@@ -0,0 +1,42 @@
|
||||
global:
|
||||
scrape_interval: 60s
|
||||
scrape_timeout: 10s
|
||||
evaluation_interval: 15s
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets: []
|
||||
scheme: http
|
||||
timeout: 10s
|
||||
api_version: v1
|
||||
scrape_configs:
|
||||
- job_name: node
|
||||
static_configs:
|
||||
- targets: ['node-exporter:9100']
|
||||
- job_name: keeper
|
||||
honor_timestamps: true
|
||||
scrape_interval: 15s
|
||||
scrape_timeout: 10s
|
||||
metrics_path: /metrics
|
||||
scheme: http
|
||||
static_configs:
|
||||
- targets:
|
||||
- keeper:9464
|
||||
- job_name: wallet
|
||||
honor_timestamps: true
|
||||
scrape_interval: 15s
|
||||
scrape_timeout: 10s
|
||||
metrics_path: /metrics/${wallet_address}
|
||||
scheme: http
|
||||
static_configs:
|
||||
- targets:
|
||||
- wallet-tracker:3000
|
||||
- job_name: user
|
||||
honor_timestamps: true
|
||||
scrape_interval: 15s
|
||||
scrape_timeout: 10s
|
||||
metrics_path: /metrics
|
||||
scheme: http
|
||||
static_configs:
|
||||
- targets:
|
||||
- user-metrics:3000
|
||||
2
templates/monitoring/prometheus/web.yml.tpl
Normal file
2
templates/monitoring/prometheus/web.yml.tpl
Normal file
@@ -0,0 +1,2 @@
|
||||
basic_auth_users:
|
||||
prom: ${prometheus_password_bcrypt}
|
||||
1
user-metrics/.gitignore
vendored
Normal file
1
user-metrics/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
7
user-metrics/Dockerfile
Normal file
7
user-metrics/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
COPY src src
|
||||
CMD ["npm", "start"]
|
||||
2278
user-metrics/package-lock.json
generated
Normal file
2278
user-metrics/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
user-metrics/package.json
Normal file
20
user-metrics/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "dex-metrics",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node --no-warnings=ExperimentalWarning src/main.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"prom-client": "^15.1.0",
|
||||
"@drift-labs/sdk": "2.78.0-beta.0",
|
||||
"@solana/web3.js": "^1.89.1",
|
||||
"bigint-buffer": "^1.1.5",
|
||||
"dotenv": "^16.4.4"
|
||||
}
|
||||
}
|
||||
94
user-metrics/src/main.js
Normal file
94
user-metrics/src/main.js
Normal file
@@ -0,0 +1,94 @@
|
||||
require('dotenv').config()
|
||||
const web3 = require("@solana/web3.js");
|
||||
const drift = require("@drift-labs/sdk");
|
||||
const express = require('express');
|
||||
const { createMetrics } = require('./metrics');
|
||||
|
||||
|
||||
const LAMPORTS_PER_SOL = 1000000000;
|
||||
|
||||
const USDC_INT = 1000000;
|
||||
|
||||
const USDC_MARKET = 0;
|
||||
const SOL_MARKET = 1;
|
||||
|
||||
const SOL_MINT_ADDRESS = 'So11111111111111111111111111111111111111112';
|
||||
const USDC_MINT_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||
|
||||
const USDC_MINT_PUBLIC_KEY = new web3.PublicKey(USDC_MINT_ADDRESS);
|
||||
const keyPairFile = process.env.PRIVATE_KEY || process.env.PRIVATE_KEY_FILE;
|
||||
const wallet = new drift.Wallet(drift.loadKeypair(keyPairFile));
|
||||
const connection = new web3.Connection(process.env.RPC_ENDPOINT);
|
||||
|
||||
const [registry, metrics] = createMetrics();
|
||||
|
||||
const app = express();
|
||||
|
||||
const driftClient = new drift.DriftClient({
|
||||
connection,
|
||||
wallet,
|
||||
env: 'mainnet-beta',
|
||||
activeSubAccountId: 0,
|
||||
subAccountIds: [0],
|
||||
});
|
||||
|
||||
const log = (msg) => {
|
||||
console.log(`[${new Date().toISOString()}] ${msg}`)
|
||||
};
|
||||
|
||||
const trimWalletAddress = (walletAddress) => {
|
||||
return `${walletAddress.slice(0,4)}...${walletAddress.slice(walletAddress.length-4, walletAddress.length)}`;
|
||||
}
|
||||
|
||||
|
||||
const quoteNumber = (val) => {
|
||||
return drift.convertToNumber(val, drift.QUOTE_PRECISION);
|
||||
}
|
||||
|
||||
const baseNumber = (val) => {
|
||||
return drift.convertToNumber(val, drift.BASE_PRECISION);
|
||||
}
|
||||
|
||||
const getWalletBalance = async (connection, publicKey) => {
|
||||
const lamportsBalance = await connection.getBalance(publicKey);
|
||||
return lamportsBalance / LAMPORTS_PER_SOL;
|
||||
};
|
||||
|
||||
const getUsdcBalance = async (connection, publicKey) => {
|
||||
const balance = await connection.getParsedTokenAccountsByOwner(
|
||||
publicKey, { mint: USDC_MINT_PUBLIC_KEY }
|
||||
);
|
||||
return balance.value[0]?.account.data.parsed.info.tokenAmount.uiAmount;
|
||||
};
|
||||
|
||||
const init = async() => {
|
||||
await driftClient.subscribe();
|
||||
log('DriftClient initialized');
|
||||
ready = true;
|
||||
};
|
||||
|
||||
let ready = false;
|
||||
|
||||
app.get('/metrics', async (req, res) => {
|
||||
res.setHeader('Content-Type', registry.contentType);
|
||||
registry.resetMetrics();
|
||||
|
||||
if(ready){
|
||||
let user = driftClient.getUser();
|
||||
let label = { wallet: wallet.publicKey.toString(), walletShort: trimWalletAddress(wallet.publicKey.toString()) };
|
||||
|
||||
metrics.totalCollateral.set(label, quoteNumber(user.getTotalCollateral()));
|
||||
metrics.unrealizedPNL.set(label, quoteNumber(user.getUnrealizedPNL()));
|
||||
metrics.solBalance.set(label, await getWalletBalance(connection, wallet.publicKey));
|
||||
metrics.usdcBalance.set(label, await getUsdcBalance(connection, wallet.publicKey));
|
||||
}
|
||||
|
||||
res.send(await registry.metrics());
|
||||
|
||||
});
|
||||
|
||||
app.listen(3000, () => {
|
||||
log("Server is running on port 3000");
|
||||
init();
|
||||
});
|
||||
|
||||
42
user-metrics/src/metrics.js
Normal file
42
user-metrics/src/metrics.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const client = require('prom-client');
|
||||
|
||||
const createMetrics = () => {
|
||||
const registry = new client.Registry();
|
||||
|
||||
const solBalanceMetric = new client.Gauge({
|
||||
name: "sol_balance",
|
||||
help: "SOL Balance",
|
||||
labelNames: ['wallet','walletShort']
|
||||
});
|
||||
const usdcBalanceMetric = new client.Gauge({
|
||||
name: "usdc_balance",
|
||||
help: "USDC Balance",
|
||||
labelNames: ['wallet','walletShort']
|
||||
});
|
||||
const totalCollateralMetric = new client.Gauge({
|
||||
name: "total_collateral",
|
||||
help: "Total Collateral",
|
||||
labelNames: ['wallet','walletShort']
|
||||
});
|
||||
const unrealizedPNLMetric = new client.Gauge({
|
||||
name: "unrealized_pnl",
|
||||
help: "Unrealized PNL",
|
||||
labelNames: ['wallet','walletShort']
|
||||
});
|
||||
|
||||
registry.registerMetric(usdcBalanceMetric);
|
||||
registry.registerMetric(solBalanceMetric);
|
||||
registry.registerMetric(totalCollateralMetric);
|
||||
registry.registerMetric(unrealizedPNLMetric);
|
||||
|
||||
return [registry, {
|
||||
usdcBalance: usdcBalanceMetric,
|
||||
solBalance: solBalanceMetric,
|
||||
totalCollateral: totalCollateralMetric,
|
||||
unrealizedPNL: unrealizedPNLMetric
|
||||
}];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createMetrics
|
||||
}
|
||||
88
variables.tf
Normal file
88
variables.tf
Normal file
@@ -0,0 +1,88 @@
|
||||
variable "do_token" {
|
||||
description = "DigitalOcean access token"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "linode_token" {
|
||||
description = "Linode access token"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "bot" {
|
||||
description = "Bot configuration"
|
||||
type = object({
|
||||
wallet_address = string
|
||||
rpc_endpoint = string
|
||||
ws_endpoint = string
|
||||
keeper_private_key = string
|
||||
jito_private_key = string
|
||||
docker_image = string
|
||||
docker_image_wallet_tracker = string
|
||||
})
|
||||
}
|
||||
|
||||
variable "monitoring" {
|
||||
description = "Monitoring configuration"
|
||||
default = {
|
||||
grafana_user = "admin"
|
||||
grafana_password = "grafana"
|
||||
prometheus_password = "prompass"
|
||||
}
|
||||
type = object({
|
||||
grafana_user = string
|
||||
grafana_password = string
|
||||
prometheus_password = string
|
||||
})
|
||||
}
|
||||
|
||||
variable "linode_instances" {
|
||||
description = "List of server configurations for Linode"
|
||||
default = [
|
||||
{
|
||||
label = "DK-LN-AMS"
|
||||
group = "keeper"
|
||||
image = "linode/ubuntu23.10"
|
||||
region = "nl-ams"
|
||||
type = "g6-nanode-1"
|
||||
ntp_server = "ntp.amsterdam.jito.wtf"
|
||||
jito_block_engine_url = "amsterdam.mainnet.block-engine.jito.wtf"
|
||||
use_jito = true
|
||||
},
|
||||
{
|
||||
label = "DK-LN-OSA"
|
||||
group = "keeper"
|
||||
image = "linode/ubuntu23.10"
|
||||
region = "jp-osa"
|
||||
type = "g6-nanode-1"
|
||||
ntp_server = "ntp.tokyo.jito.wtf"
|
||||
jito_block_engine_url = "tokyo.mainnet.block-engine.jito.wtf"
|
||||
use_jito = true
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
variable "digitalocean_instances" {
|
||||
description = "List of server configurations for DigitalOcean"
|
||||
default = [
|
||||
{
|
||||
label = "DK-DO-FRA"
|
||||
image = "ubuntu-23-10-x64"
|
||||
region = "fra1"
|
||||
type = "s-1vcpu-1gb"
|
||||
ntp_server = "ntp.frankfurt.jito.wtf"
|
||||
jito_block_engine_url = "frankfurt.mainnet.block-engine.jito.wtf"
|
||||
use_jito = true
|
||||
},
|
||||
{
|
||||
label = "DK-DO-NYC"
|
||||
image = "ubuntu-23-10-x64"
|
||||
region = "nyc1"
|
||||
type = "s-1vcpu-1gb"
|
||||
ntp_server = "ntp.ny.jito.wtf"
|
||||
jito_block_engine_url = "ny.mainnet.block-engine.jito.wtf"
|
||||
use_jito = true
|
||||
}
|
||||
]
|
||||
}
|
||||
1
wallet-tracker/.gitignore
vendored
1
wallet-tracker/.gitignore
vendored
@@ -1,2 +1 @@
|
||||
|
||||
node_modules/
|
||||
|
||||
18
wallet-tracker/README.md
Normal file
18
wallet-tracker/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Wallet-Tracker
|
||||
|
||||
## Endpoints
|
||||
|
||||
Metrics: /metrics/:wallet-address?
|
||||
If a `wallet-address` is provided in the URL, the app will fetch the metrics states from the given address.
|
||||
|
||||
If only 1 wallet needs to be tracked, it is also possible to set the `WALLET_ADDRESS` environment variable and just call the `/metrics` path.
|
||||
|
||||
## Metrics
|
||||
|
||||
Jupiter:
|
||||
- SOL Price
|
||||
|
||||
Solana:
|
||||
- SOL Balance
|
||||
- USDC Balance
|
||||
- SOL Balance in USDC
|
||||
@@ -3,24 +3,35 @@ const { createMetrics } = require('./metrics');
|
||||
const { loadWalletBalance, loadUSDCBalance, loadSolanaMarketData, extractWalletBalance, extractUSDCBalance, extractSOLPrice } = require('./solana');
|
||||
|
||||
const WALLET_ADDRESS = process.env.WALLET_ADDRESS;
|
||||
const [registry, usdcBalanceMetric, solBalanceMetric, solUsdcBalanceMetric] = createMetrics();
|
||||
const [registry, usdcBalanceMetric, solBalanceMetric, solUsdcBalanceMetric, solPriceMetric] = createMetrics();
|
||||
const app = express();
|
||||
|
||||
app.get('/metrics', async (req, res) => {
|
||||
const trimWalletAddress = (walletAddress) => {
|
||||
return `${walletAddress.slice(0,4)}...${walletAddress.slice(walletAddress.length-4, walletAddress.length)}`;
|
||||
}
|
||||
|
||||
app.get('/metrics/:addr?', async (req, res) => {
|
||||
const walletAddress = req.params.addr || WALLET_ADDRESS;
|
||||
console.log(`Gathering metrics for ${walletAddress}`);
|
||||
res.setHeader('Content-Type', registry.contentType);
|
||||
|
||||
registry.resetMetrics();
|
||||
|
||||
let [solBalance, usdcBalance, marketData] = await Promise.all([
|
||||
loadWalletBalance(WALLET_ADDRESS),
|
||||
loadUSDCBalance(WALLET_ADDRESS),
|
||||
loadWalletBalance(walletAddress),
|
||||
loadUSDCBalance(walletAddress),
|
||||
loadSolanaMarketData()]);
|
||||
|
||||
solBalanceMetric.set({ wallet: WALLET_ADDRESS}, extractWalletBalance(solBalance));
|
||||
usdcBalanceMetric.set({ wallet: WALLET_ADDRESS}, extractUSDCBalance(usdcBalance));
|
||||
solUsdcBalanceMetric.set({ wallet: WALLET_ADDRESS}, extractWalletBalance(solBalance) * extractSOLPrice(marketData));
|
||||
|
||||
let label = { wallet: walletAddress, walletShort: trimWalletAddress(walletAddress) };
|
||||
|
||||
solBalanceMetric.set(label, extractWalletBalance(solBalance));
|
||||
usdcBalanceMetric.set(label, extractUSDCBalance(usdcBalance));
|
||||
solUsdcBalanceMetric.set(label, extractWalletBalance(solBalance) * extractSOLPrice(marketData));
|
||||
solPriceMetric.set(extractSOLPrice(marketData));
|
||||
|
||||
res.send(await registry.metrics());
|
||||
});
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log("Server is running on port 3000");
|
||||
});
|
||||
});
|
||||
@@ -6,26 +6,32 @@ const createMetrics = () => {
|
||||
const solBalanceMetric = new client.Gauge({
|
||||
name: "sol_balance",
|
||||
help: "SOL Balance",
|
||||
labelNames: ['wallet']
|
||||
labelNames: ['wallet','walletShort']
|
||||
});
|
||||
const usdcBalanceMetric = new client.Gauge({
|
||||
name: "usdc_balance",
|
||||
help: "USDC Balance",
|
||||
labelNames: ['wallet']
|
||||
labelNames: ['wallet','walletShort']
|
||||
});
|
||||
const solUsdcBalanceMetric = new client.Gauge({
|
||||
name: "sol_usdc_balance",
|
||||
help: "SOL Balance in USDC",
|
||||
labelNames: ['wallet']
|
||||
labelNames: ['wallet','walletShort']
|
||||
});
|
||||
|
||||
const solPriceMetric = new client.Gauge({
|
||||
name: "sol_price",
|
||||
help: "SOL Price in USDC"
|
||||
});
|
||||
|
||||
registry.registerMetric(usdcBalanceMetric);
|
||||
registry.registerMetric(solBalanceMetric);
|
||||
registry.registerMetric(solUsdcBalanceMetric);
|
||||
registry.registerMetric(solPriceMetric);
|
||||
|
||||
return [registry, usdcBalanceMetric, solBalanceMetric, solUsdcBalanceMetric];
|
||||
return [registry, usdcBalanceMetric, solBalanceMetric, solUsdcBalanceMetric, solPriceMetric];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createMetrics
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user