Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mainnet #323

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
"main": "lib/index.js",
"license": "Apache-2.0",
"dependencies": {
"@drift-labs/jit-proxy": "0.12.17",
"@drift-labs/sdk": "2.104.0-beta.30",
"@drift-labs/jit-proxy": "0.12.21",
"@drift-labs/sdk": "2.104.0-beta.34",
"@opentelemetry/api": "1.7.0",
"@opentelemetry/auto-instrumentations-node": "0.31.2",
"@opentelemetry/exporter-prometheus": "0.31.0",
"@opentelemetry/sdk-node": "0.31.0",
"@project-serum/anchor": "0.19.1-beta.1",
"@project-serum/serum": "0.13.65",
"@pythnetwork/price-service-client": "1.9.0",
"@pythnetwork/pyth-lazer-sdk": "^0.1.1",
"@solana/spl-token": "0.3.7",
"@solana/web3.js": "1.92.3",
"@types/bn.js": "5.1.5",
Expand Down
26 changes: 20 additions & 6 deletions src/bots/liquidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
calculateAccountValueUsd,
checkIfAccountExists,
handleSimResultError,
isSolLstToken,
simulateAndGetTxWithCUs,
SimulateAndGetTxWithCUsResponse,
} from '../utils';
Expand Down Expand Up @@ -1259,10 +1260,17 @@ export class LiquidatorBot implements Bot {
let outMarket: SpotMarketAccount | undefined;
let inMarket: SpotMarketAccount | undefined;
let amountIn: BN | undefined;
const spotMarketIsSolLst = isSolLstToken(spotMarketIndex);
if (isVariant(orderDirection, 'long')) {
// sell USDC, buy spotMarketIndex
inMarket = this.driftClient.getSpotMarketAccount(0);
outMarket = this.driftClient.getSpotMarketAccount(spotMarketIndex);
if (spotMarketIsSolLst) {
// sell SOL, buy the LST
inMarket = this.driftClient.getSpotMarketAccount(1);
outMarket = this.driftClient.getSpotMarketAccount(spotMarketIndex);
} else {
// sell USDC, buy spotMarketIndex
inMarket = this.driftClient.getSpotMarketAccount(0);
outMarket = this.driftClient.getSpotMarketAccount(spotMarketIndex);
}
if (!inMarket || !outMarket) {
logger.error('failed to get spot markets');
return undefined;
Expand All @@ -1274,9 +1282,15 @@ export class LiquidatorBot implements Bot {
.mul(inPrecision)
.div(PRICE_PRECISION.mul(outPrecision));
} else {
// sell spotMarketIndex, buy USDC
inMarket = this.driftClient.getSpotMarketAccount(spotMarketIndex);
outMarket = this.driftClient.getSpotMarketAccount(0);
if (spotMarketIsSolLst) {
// sell spotMarketIndex, buy SOL
inMarket = this.driftClient.getSpotMarketAccount(spotMarketIndex);
outMarket = this.driftClient.getSpotMarketAccount(1);
} else {
// sell spotMarketIndex, buy USDC
inMarket = this.driftClient.getSpotMarketAccount(spotMarketIndex);
outMarket = this.driftClient.getSpotMarketAccount(0);
}
amountIn = baseAmountIn;
}

Expand Down
268 changes: 268 additions & 0 deletions src/bots/pythLazerCranker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { Bot } from '../types';
import { logger } from '../logger';
import { GlobalConfig, PythLazerCrankerBotConfig } from '../config';
import { PriceUpdateAccount } from '@pythnetwork/pyth-solana-receiver/lib/PythSolanaReceiver';
import {
BlockhashSubscriber,
DriftClient,
getOracleClient,
getPythLazerOraclePublicKey,
OracleClient,
OracleSource,
PriorityFeeSubscriber,
TxSigAndSlot,
} from '@drift-labs/sdk';
import { BundleSender } from '../bundleSender';
import {
AddressLookupTableAccount,
ComputeBudgetProgram,
} from '@solana/web3.js';
import { chunks, simulateAndGetTxWithCUs, sleepMs } from '../utils';
import { Agent, setGlobalDispatcher } from 'undici';
import { PythLazerClient } from '@pythnetwork/pyth-lazer-sdk';

setGlobalDispatcher(
new Agent({
connections: 200,
})
);

const SIM_CU_ESTIMATE_MULTIPLIER = 1.5;

export class PythLazerCrankerBot implements Bot {
private wsClient: PythLazerClient;
private pythOracleClient: OracleClient;
readonly decodeFunc: (name: string, data: Buffer) => PriceUpdateAccount;

public name: string;
public dryRun: boolean;
private intervalMs: number;
private feedIdChunkToPriceMessage: Map<number[], string> = new Map();
public defaultIntervalMs = 30_000;

private blockhashSubscriber: BlockhashSubscriber;
private health: boolean = true;
private slotStalenessThresholdRestart: number = 300;
private txSuccessRateThreshold: number = 0.5;

constructor(
private globalConfig: GlobalConfig,
private crankConfigs: PythLazerCrankerBotConfig,
private driftClient: DriftClient,
private priorityFeeSubscriber?: PriorityFeeSubscriber,
private bundleSender?: BundleSender,
private lookupTableAccounts: AddressLookupTableAccount[] = []
) {
this.name = crankConfigs.botId;
this.dryRun = crankConfigs.dryRun;
this.intervalMs = crankConfigs.intervalMs;
if (!globalConfig.hermesEndpoint) {
throw new Error('Missing hermesEndpoint in global config');
}

if (globalConfig.driftEnv != 'devnet') {
throw new Error('Only devnet drift env is supported');
}

const hermesEndpointParts = globalConfig.hermesEndpoint.split('?token=');
this.wsClient = new PythLazerClient(
hermesEndpointParts[0],
hermesEndpointParts[1]
);

this.pythOracleClient = getOracleClient(
OracleSource.PYTH_LAZER,
driftClient.connection,
driftClient.program
);
this.decodeFunc =
this.driftClient.program.account.pythLazerOracle.coder.accounts.decodeUnchecked.bind(
this.driftClient.program.account.pythLazerOracle.coder.accounts
);

this.blockhashSubscriber = new BlockhashSubscriber({
connection: driftClient.connection,
});
this.txSuccessRateThreshold = crankConfigs.txSuccessRateThreshold;
this.slotStalenessThresholdRestart =
crankConfigs.slotStalenessThresholdRestart;
}

async init(): Promise<void> {
logger.info(`Initializing ${this.name} bot`);
await this.blockhashSubscriber.subscribe();
this.lookupTableAccounts.push(
await this.driftClient.fetchMarketLookupTableAccount()
);

const updateConfigs = this.crankConfigs.updateConfigs;

let subscriptionId = 1;
for (const configChunk of chunks(Object.keys(updateConfigs), 11)) {
const priceFeedIds: number[] = configChunk.map((alias) => {
return updateConfigs[alias].feedId;
});

const sendMessage = () =>
this.wsClient.send({
type: 'subscribe',
subscriptionId,
priceFeedIds,
properties: ['price'],
chains: ['solana'],
deliveryFormat: 'json',
channel: 'fixed_rate@200ms',
jsonBinaryEncoding: 'hex',
});
if (this.wsClient.ws.readyState != 1) {
this.wsClient.ws.addEventListener('open', () => {
sendMessage();
});
} else {
sendMessage();
}

this.wsClient.addMessageListener((message) => {
switch (message.type) {
case 'json': {
if (message.value.type == 'streamUpdated') {
if (message.value.solana?.data)
this.feedIdChunkToPriceMessage.set(
priceFeedIds,
message.value.solana.data
);
}
break;
}
default: {
break;
}
}
});
subscriptionId++;
}

this.priorityFeeSubscriber?.updateAddresses(
Object.keys(this.feedIdChunkToPriceMessage)
.flat()
.map((feedId) =>
getPythLazerOraclePublicKey(
this.driftClient.program.programId,
Number(feedId)
)
)
);
}

async reset(): Promise<void> {
logger.info(`Resetting ${this.name} bot`);
this.blockhashSubscriber.unsubscribe();
await this.driftClient.unsubscribe();
this.wsClient.ws.close();
}

async startIntervalLoop(intervalMs = this.intervalMs): Promise<void> {
logger.info(`Starting ${this.name} bot with interval ${intervalMs} ms`);
await sleepMs(5000);
await this.runCrankLoop();

setInterval(async () => {
await this.runCrankLoop();
}, intervalMs);
}

private async getBlockhashForTx(): Promise<string> {
const cachedBlockhash = this.blockhashSubscriber.getLatestBlockhash(10);
if (cachedBlockhash) {
return cachedBlockhash.blockhash as string;
}

const recentBlockhash =
await this.driftClient.connection.getLatestBlockhash({
commitment: 'confirmed',
});

return recentBlockhash.blockhash;
}

async runCrankLoop() {
for (const [
feedIds,
priceMessage,
] of this.feedIdChunkToPriceMessage.entries()) {
const ixs = [
ComputeBudgetProgram.setComputeUnitLimit({
units: 1_400_000,
}),
];
if (this.globalConfig.useJito) {
ixs.push(this.bundleSender!.getTipIx());
const simResult = await simulateAndGetTxWithCUs({
ixs,
connection: this.driftClient.connection,
payerPublicKey: this.driftClient.wallet.publicKey,
lookupTableAccounts: this.lookupTableAccounts,
cuLimitMultiplier: SIM_CU_ESTIMATE_MULTIPLIER,
doSimulation: true,
recentBlockhash: await this.getBlockhashForTx(),
});
simResult.tx.sign([
// @ts-ignore
this.driftClient.wallet.payer,
]);
this.bundleSender?.sendTransactions(
[simResult.tx],
undefined,
undefined,
false
);
} else {
const priorityFees = Math.floor(
(this.priorityFeeSubscriber?.getCustomStrategyResult() || 0) *
this.driftClient.txSender.getSuggestedPriorityFeeMultiplier()
);
logger.info(
`Priority fees to use: ${priorityFees} with multiplier: ${this.driftClient.txSender.getSuggestedPriorityFeeMultiplier()}`
);
ixs.push(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFees,
})
);
}
const pythLazerIxs =
await this.driftClient.getPostPythLazerOracleUpdateIxs(
feedIds,
priceMessage,
ixs
);
ixs.push(...pythLazerIxs);
const simResult = await simulateAndGetTxWithCUs({
ixs,
connection: this.driftClient.connection,
payerPublicKey: this.driftClient.wallet.publicKey,
lookupTableAccounts: this.lookupTableAccounts,
cuLimitMultiplier: SIM_CU_ESTIMATE_MULTIPLIER,
doSimulation: true,
recentBlockhash: await this.getBlockhashForTx(),
});
const startTime = Date.now();
this.driftClient
.sendTransaction(simResult.tx)
.then((txSigAndSlot: TxSigAndSlot) => {
logger.info(
`Posted pyth lazer oracles for ${feedIds} update atomic tx: ${
txSigAndSlot.txSig
}, took ${Date.now() - startTime}ms`
);
})
.catch((e) => {
console.log(e);
});
}
}

async healthCheck(): Promise<boolean> {
return this.health;
}
}
12 changes: 12 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ export type PythCrankerBotConfig = BaseBotConfig & {
};
};

export type PythLazerCrankerBotConfig = BaseBotConfig & {
slotStalenessThresholdRestart: number;
txSuccessRateThreshold: number;
intervalMs: number;
updateConfigs: {
[key: string]: {
feedId: number;
};
};
};

export type SwitchboardCrankerBotConfig = BaseBotConfig & {
intervalMs: number;
queuePubkey: string;
Expand Down Expand Up @@ -135,6 +146,7 @@ export type BotConfigMap = {
userIdleFlipper?: BaseBotConfig;
markTwapCrank?: BaseBotConfig;
pythCranker?: PythCrankerBotConfig;
pythLazerCranker?: PythLazerCrankerBotConfig;
switchboardCranker?: SwitchboardCrankerBotConfig;
swiftTaker?: BaseBotConfig;
swiftMaker?: BaseBotConfig;
Expand Down
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { webhookMessage } from './webhook';
import { PythPriceFeedSubscriber } from './pythPriceFeedSubscriber';
import { PythCrankerBot } from './bots/pythCranker';
import { SwitchboardCrankerBot } from './bots/switchboardCranker';
import { PythLazerCrankerBot } from './bots/pythLazerCranker';

require('dotenv').config();
const commitHash = process.env.COMMIT ?? '';
Expand Down Expand Up @@ -562,6 +563,19 @@ const runBot = async () => {
)
);
}
if (configHasBot(config, 'pythLazerCranker')) {
needPriorityFeeSubscriber = true;
bots.push(
new PythLazerCrankerBot(
config.global,
config.botConfigs!.pythLazerCranker!,
driftClient,
priorityFeeSubscriber,
bundleSender,
[]
)
);
}
if (configHasBot(config, 'switchboardCranker')) {
needPriorityFeeSubscriber = true;
needDriftStateWatcher = true;
Expand Down
Loading