-
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.
* Bumping sdk and jit dependencies to 2.104.0-beta.31 and 0.12.18 * Bumping sdk and jit dependencies to 2.104.0-beta.32 and 0.12.19 * 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 * Bumping sdk and jit dependencies to 2.104.0-beta.33 and 0.12.20 * Bumping sdk and jit dependencies to 2.104.0-beta.34 and 0.12.21 * liquidator: use SOL routes when swapping LSTs (#322) --------- Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: moosecat <[email protected]>
- Loading branch information
1 parent
d31425e
commit 9832991
Showing
7 changed files
with
351 additions
and
18 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
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
Oops, something went wrong.