Skip to content

Commit

Permalink
Nour/pyth lazer cranker (#321)
Browse files Browse the repository at this point in the history
* add pyth lazer cranker

* rm unncessary libraries

* remove unnecessary code

* add entrypoint for the cranker

* added improvements

* increase the chunk size
  • Loading branch information
NourAlharithi authored Dec 18, 2024
1 parent 6c7abeb commit f6bdbf9
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@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
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
17 changes: 15 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,15 @@
dependencies:
bn.js "^5.2.1"

"@pythnetwork/[email protected]":
"@pythnetwork/pyth-lazer-sdk@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@pythnetwork/pyth-lazer-sdk/-/pyth-lazer-sdk-0.1.1.tgz#5242c04f9b4f6ee0d3cc1aad228dfcb85b5e6498"
integrity sha512-/Zr9qbNi9YZb9Nl3ilkUKgeSQovevsXV57pIGrw04NFUmK4Ua92o2SyK8RRaqcw8zYtiDbseU1CgWHCfGYjRRQ==
dependencies:
isomorphic-ws "^5.0.0"
ws "^8.18.0"

"@pythnetwork/pyth-solana-receiver@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@pythnetwork/pyth-solana-receiver/-/pyth-solana-receiver-0.7.0.tgz#253a0d15a135d625ceca7ba1b47940dd03b9cab6"
integrity sha512-OoEAHh92RPRdKkfjkcKGrjC+t0F3SEL754iKFmixN9zyS8pIfZSVfFntmkHa9pWmqEMxdx/i925a8B5ny8Tuvg==
Expand Down Expand Up @@ -3703,6 +3711,11 @@ isomorphic-ws@^4.0.1:
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==

isomorphic-ws@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf"
integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==

jayson@^4.0.0, jayson@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.0.tgz#60dc946a85197317f2b1439d672a8b0a99cea2f9"
Expand Down Expand Up @@ -5236,7 +5249,7 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==

[email protected]:
[email protected], ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
Expand Down

0 comments on commit f6bdbf9

Please sign in to comment.