Skip to content

Commit f6bdbf9

Browse files
authored
Nour/pyth lazer cranker (#321)
* add pyth lazer cranker * rm unncessary libraries * remove unnecessary code * add entrypoint for the cranker * added improvements * increase the chunk size
1 parent 6c7abeb commit f6bdbf9

File tree

5 files changed

+310
-2
lines changed

5 files changed

+310
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@project-serum/anchor": "0.19.1-beta.1",
1515
"@project-serum/serum": "0.13.65",
1616
"@pythnetwork/price-service-client": "1.9.0",
17+
"@pythnetwork/pyth-lazer-sdk": "^0.1.1",
1718
"@solana/spl-token": "0.3.7",
1819
"@solana/web3.js": "1.92.3",
1920
"@types/bn.js": "5.1.5",

src/bots/pythLazerCranker.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import { Bot } from '../types';
2+
import { logger } from '../logger';
3+
import { GlobalConfig, PythLazerCrankerBotConfig } from '../config';
4+
import { PriceUpdateAccount } from '@pythnetwork/pyth-solana-receiver/lib/PythSolanaReceiver';
5+
import {
6+
BlockhashSubscriber,
7+
DriftClient,
8+
getOracleClient,
9+
getPythLazerOraclePublicKey,
10+
OracleClient,
11+
OracleSource,
12+
PriorityFeeSubscriber,
13+
TxSigAndSlot,
14+
} from '@drift-labs/sdk';
15+
import { BundleSender } from '../bundleSender';
16+
import {
17+
AddressLookupTableAccount,
18+
ComputeBudgetProgram,
19+
} from '@solana/web3.js';
20+
import { chunks, simulateAndGetTxWithCUs, sleepMs } from '../utils';
21+
import { Agent, setGlobalDispatcher } from 'undici';
22+
import { PythLazerClient } from '@pythnetwork/pyth-lazer-sdk';
23+
24+
setGlobalDispatcher(
25+
new Agent({
26+
connections: 200,
27+
})
28+
);
29+
30+
const SIM_CU_ESTIMATE_MULTIPLIER = 1.5;
31+
32+
export class PythLazerCrankerBot implements Bot {
33+
private wsClient: PythLazerClient;
34+
private pythOracleClient: OracleClient;
35+
readonly decodeFunc: (name: string, data: Buffer) => PriceUpdateAccount;
36+
37+
public name: string;
38+
public dryRun: boolean;
39+
private intervalMs: number;
40+
private feedIdChunkToPriceMessage: Map<number[], string> = new Map();
41+
public defaultIntervalMs = 30_000;
42+
43+
private blockhashSubscriber: BlockhashSubscriber;
44+
private health: boolean = true;
45+
private slotStalenessThresholdRestart: number = 300;
46+
private txSuccessRateThreshold: number = 0.5;
47+
48+
constructor(
49+
private globalConfig: GlobalConfig,
50+
private crankConfigs: PythLazerCrankerBotConfig,
51+
private driftClient: DriftClient,
52+
private priorityFeeSubscriber?: PriorityFeeSubscriber,
53+
private bundleSender?: BundleSender,
54+
private lookupTableAccounts: AddressLookupTableAccount[] = []
55+
) {
56+
this.name = crankConfigs.botId;
57+
this.dryRun = crankConfigs.dryRun;
58+
this.intervalMs = crankConfigs.intervalMs;
59+
if (!globalConfig.hermesEndpoint) {
60+
throw new Error('Missing hermesEndpoint in global config');
61+
}
62+
63+
if (globalConfig.driftEnv != 'devnet') {
64+
throw new Error('Only devnet drift env is supported');
65+
}
66+
67+
const hermesEndpointParts = globalConfig.hermesEndpoint.split('?token=');
68+
this.wsClient = new PythLazerClient(
69+
hermesEndpointParts[0],
70+
hermesEndpointParts[1]
71+
);
72+
73+
this.pythOracleClient = getOracleClient(
74+
OracleSource.PYTH_LAZER,
75+
driftClient.connection,
76+
driftClient.program
77+
);
78+
this.decodeFunc =
79+
this.driftClient.program.account.pythLazerOracle.coder.accounts.decodeUnchecked.bind(
80+
this.driftClient.program.account.pythLazerOracle.coder.accounts
81+
);
82+
83+
this.blockhashSubscriber = new BlockhashSubscriber({
84+
connection: driftClient.connection,
85+
});
86+
this.txSuccessRateThreshold = crankConfigs.txSuccessRateThreshold;
87+
this.slotStalenessThresholdRestart =
88+
crankConfigs.slotStalenessThresholdRestart;
89+
}
90+
91+
async init(): Promise<void> {
92+
logger.info(`Initializing ${this.name} bot`);
93+
await this.blockhashSubscriber.subscribe();
94+
this.lookupTableAccounts.push(
95+
await this.driftClient.fetchMarketLookupTableAccount()
96+
);
97+
98+
const updateConfigs = this.crankConfigs.updateConfigs;
99+
100+
let subscriptionId = 1;
101+
for (const configChunk of chunks(Object.keys(updateConfigs), 11)) {
102+
const priceFeedIds: number[] = configChunk.map((alias) => {
103+
return updateConfigs[alias].feedId;
104+
});
105+
106+
const sendMessage = () =>
107+
this.wsClient.send({
108+
type: 'subscribe',
109+
subscriptionId,
110+
priceFeedIds,
111+
properties: ['price'],
112+
chains: ['solana'],
113+
deliveryFormat: 'json',
114+
channel: 'fixed_rate@200ms',
115+
jsonBinaryEncoding: 'hex',
116+
});
117+
if (this.wsClient.ws.readyState != 1) {
118+
this.wsClient.ws.addEventListener('open', () => {
119+
sendMessage();
120+
});
121+
} else {
122+
sendMessage();
123+
}
124+
125+
this.wsClient.addMessageListener((message) => {
126+
switch (message.type) {
127+
case 'json': {
128+
if (message.value.type == 'streamUpdated') {
129+
if (message.value.solana?.data)
130+
this.feedIdChunkToPriceMessage.set(
131+
priceFeedIds,
132+
message.value.solana.data
133+
);
134+
}
135+
break;
136+
}
137+
default: {
138+
break;
139+
}
140+
}
141+
});
142+
subscriptionId++;
143+
}
144+
145+
this.priorityFeeSubscriber?.updateAddresses(
146+
Object.keys(this.feedIdChunkToPriceMessage)
147+
.flat()
148+
.map((feedId) =>
149+
getPythLazerOraclePublicKey(
150+
this.driftClient.program.programId,
151+
Number(feedId)
152+
)
153+
)
154+
);
155+
}
156+
157+
async reset(): Promise<void> {
158+
logger.info(`Resetting ${this.name} bot`);
159+
this.blockhashSubscriber.unsubscribe();
160+
await this.driftClient.unsubscribe();
161+
this.wsClient.ws.close();
162+
}
163+
164+
async startIntervalLoop(intervalMs = this.intervalMs): Promise<void> {
165+
logger.info(`Starting ${this.name} bot with interval ${intervalMs} ms`);
166+
await sleepMs(5000);
167+
await this.runCrankLoop();
168+
169+
setInterval(async () => {
170+
await this.runCrankLoop();
171+
}, intervalMs);
172+
}
173+
174+
private async getBlockhashForTx(): Promise<string> {
175+
const cachedBlockhash = this.blockhashSubscriber.getLatestBlockhash(10);
176+
if (cachedBlockhash) {
177+
return cachedBlockhash.blockhash as string;
178+
}
179+
180+
const recentBlockhash =
181+
await this.driftClient.connection.getLatestBlockhash({
182+
commitment: 'confirmed',
183+
});
184+
185+
return recentBlockhash.blockhash;
186+
}
187+
188+
async runCrankLoop() {
189+
for (const [
190+
feedIds,
191+
priceMessage,
192+
] of this.feedIdChunkToPriceMessage.entries()) {
193+
const ixs = [
194+
ComputeBudgetProgram.setComputeUnitLimit({
195+
units: 1_400_000,
196+
}),
197+
];
198+
if (this.globalConfig.useJito) {
199+
ixs.push(this.bundleSender!.getTipIx());
200+
const simResult = await simulateAndGetTxWithCUs({
201+
ixs,
202+
connection: this.driftClient.connection,
203+
payerPublicKey: this.driftClient.wallet.publicKey,
204+
lookupTableAccounts: this.lookupTableAccounts,
205+
cuLimitMultiplier: SIM_CU_ESTIMATE_MULTIPLIER,
206+
doSimulation: true,
207+
recentBlockhash: await this.getBlockhashForTx(),
208+
});
209+
simResult.tx.sign([
210+
// @ts-ignore
211+
this.driftClient.wallet.payer,
212+
]);
213+
this.bundleSender?.sendTransactions(
214+
[simResult.tx],
215+
undefined,
216+
undefined,
217+
false
218+
);
219+
} else {
220+
const priorityFees = Math.floor(
221+
(this.priorityFeeSubscriber?.getCustomStrategyResult() || 0) *
222+
this.driftClient.txSender.getSuggestedPriorityFeeMultiplier()
223+
);
224+
logger.info(
225+
`Priority fees to use: ${priorityFees} with multiplier: ${this.driftClient.txSender.getSuggestedPriorityFeeMultiplier()}`
226+
);
227+
ixs.push(
228+
ComputeBudgetProgram.setComputeUnitPrice({
229+
microLamports: priorityFees,
230+
})
231+
);
232+
}
233+
const pythLazerIxs =
234+
await this.driftClient.getPostPythLazerOracleUpdateIxs(
235+
feedIds,
236+
priceMessage,
237+
ixs
238+
);
239+
ixs.push(...pythLazerIxs);
240+
const simResult = await simulateAndGetTxWithCUs({
241+
ixs,
242+
connection: this.driftClient.connection,
243+
payerPublicKey: this.driftClient.wallet.publicKey,
244+
lookupTableAccounts: this.lookupTableAccounts,
245+
cuLimitMultiplier: SIM_CU_ESTIMATE_MULTIPLIER,
246+
doSimulation: true,
247+
recentBlockhash: await this.getBlockhashForTx(),
248+
});
249+
const startTime = Date.now();
250+
this.driftClient
251+
.sendTransaction(simResult.tx)
252+
.then((txSigAndSlot: TxSigAndSlot) => {
253+
logger.info(
254+
`Posted pyth lazer oracles for ${feedIds} update atomic tx: ${
255+
txSigAndSlot.txSig
256+
}, took ${Date.now() - startTime}ms`
257+
);
258+
})
259+
.catch((e) => {
260+
console.log(e);
261+
});
262+
}
263+
}
264+
265+
async healthCheck(): Promise<boolean> {
266+
return this.health;
267+
}
268+
}

src/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ export type PythCrankerBotConfig = BaseBotConfig & {
108108
};
109109
};
110110

111+
export type PythLazerCrankerBotConfig = BaseBotConfig & {
112+
slotStalenessThresholdRestart: number;
113+
txSuccessRateThreshold: number;
114+
intervalMs: number;
115+
updateConfigs: {
116+
[key: string]: {
117+
feedId: number;
118+
};
119+
};
120+
};
121+
111122
export type SwitchboardCrankerBotConfig = BaseBotConfig & {
112123
intervalMs: number;
113124
queuePubkey: string;
@@ -135,6 +146,7 @@ export type BotConfigMap = {
135146
userIdleFlipper?: BaseBotConfig;
136147
markTwapCrank?: BaseBotConfig;
137148
pythCranker?: PythCrankerBotConfig;
149+
pythLazerCranker?: PythLazerCrankerBotConfig;
138150
switchboardCranker?: SwitchboardCrankerBotConfig;
139151
swiftTaker?: BaseBotConfig;
140152
swiftMaker?: BaseBotConfig;

src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import { webhookMessage } from './webhook';
7575
import { PythPriceFeedSubscriber } from './pythPriceFeedSubscriber';
7676
import { PythCrankerBot } from './bots/pythCranker';
7777
import { SwitchboardCrankerBot } from './bots/switchboardCranker';
78+
import { PythLazerCrankerBot } from './bots/pythLazerCranker';
7879

7980
require('dotenv').config();
8081
const commitHash = process.env.COMMIT ?? '';
@@ -562,6 +563,19 @@ const runBot = async () => {
562563
)
563564
);
564565
}
566+
if (configHasBot(config, 'pythLazerCranker')) {
567+
needPriorityFeeSubscriber = true;
568+
bots.push(
569+
new PythLazerCrankerBot(
570+
config.global,
571+
config.botConfigs!.pythLazerCranker!,
572+
driftClient,
573+
priorityFeeSubscriber,
574+
bundleSender,
575+
[]
576+
)
577+
);
578+
}
565579
if (configHasBot(config, 'switchboardCranker')) {
566580
needPriorityFeeSubscriber = true;
567581
needDriftStateWatcher = true;

yarn.lock

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,15 @@
11381138
dependencies:
11391139
bn.js "^5.2.1"
11401140

1141-
"@pythnetwork/[email protected]":
1141+
"@pythnetwork/pyth-lazer-sdk@^0.1.1":
1142+
version "0.1.1"
1143+
resolved "https://registry.yarnpkg.com/@pythnetwork/pyth-lazer-sdk/-/pyth-lazer-sdk-0.1.1.tgz#5242c04f9b4f6ee0d3cc1aad228dfcb85b5e6498"
1144+
integrity sha512-/Zr9qbNi9YZb9Nl3ilkUKgeSQovevsXV57pIGrw04NFUmK4Ua92o2SyK8RRaqcw8zYtiDbseU1CgWHCfGYjRRQ==
1145+
dependencies:
1146+
isomorphic-ws "^5.0.0"
1147+
ws "^8.18.0"
1148+
1149+
"@pythnetwork/pyth-solana-receiver@^0.7.0":
11421150
version "0.7.0"
11431151
resolved "https://registry.yarnpkg.com/@pythnetwork/pyth-solana-receiver/-/pyth-solana-receiver-0.7.0.tgz#253a0d15a135d625ceca7ba1b47940dd03b9cab6"
11441152
integrity sha512-OoEAHh92RPRdKkfjkcKGrjC+t0F3SEL754iKFmixN9zyS8pIfZSVfFntmkHa9pWmqEMxdx/i925a8B5ny8Tuvg==
@@ -3703,6 +3711,11 @@ isomorphic-ws@^4.0.1:
37033711
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
37043712
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
37053713

3714+
isomorphic-ws@^5.0.0:
3715+
version "5.0.0"
3716+
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf"
3717+
integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==
3718+
37063719
jayson@^4.0.0, jayson@^4.1.0:
37073720
version "4.1.0"
37083721
resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.0.tgz#60dc946a85197317f2b1439d672a8b0a99cea2f9"
@@ -5236,7 +5249,7 @@ wrappy@1:
52365249
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
52375250
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
52385251

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

0 commit comments

Comments
 (0)