-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add pyth lazer cranker * rm unncessary libraries * remove unnecessary code * add entrypoint for the cranker * added improvements * increase the chunk size
- Loading branch information
1 parent
6c7abeb
commit f6bdbf9
Showing
5 changed files
with
310 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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== | ||
|
@@ -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" | ||
|
@@ -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== | ||
|