diff --git a/.env.copy b/.env.copy index de70fb77..79f7a390 100644 --- a/.env.copy +++ b/.env.copy @@ -18,6 +18,8 @@ COMPUTE_UNIT_LIMIT=101337 COMPUTE_UNIT_PRICE=421197 # if using warp or jito executor, fee below will be applied CUSTOM_FEE=0.006 +# simulate or execute +SIMULATE_TX=true # Buy QUOTE_MINT=WSOL diff --git a/README.md b/README.md index 4e6658f8..9e74ae55 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ You should see the following output: - `CUSTOM_FEE` - If using warp or jito executors this value will be used for transaction fees instead of `COMPUTE_UNIT_LIMIT` and `COMPUTE_UNIT_LIMIT` - Minimum value is 0.0001 SOL, but we recommend using 0.006 SOL or above - On top of this fee, minimal solana network fee will be applied +- `SIMULATE_TX` - Allow simulate transactions #### Buy diff --git a/bot.ts b/bot.ts index 835aca8e..454912e9 100644 --- a/bot.ts +++ b/bot.ts @@ -23,7 +23,8 @@ import { Mutex } from 'async-mutex'; import BN from 'bn.js'; import { WarpTransactionExecutor } from './transactions/warp-transaction-executor'; import { JitoTransactionExecutor } from './transactions/jito-rpc-transaction-executor'; - +import fs from 'fs'; +import { TpuTransactionExecutor } from './transactions/tpu-transaction-executor'; export interface BotConfig { wallet: Keypair; checkRenounced: boolean; @@ -52,6 +53,7 @@ export interface BotConfig { filterCheckInterval: number; filterCheckDuration: number; consecutiveMatchCount: number; + simulateTx: boolean; } export class Bot { @@ -65,6 +67,7 @@ export class Bot { private sellExecutionCount = 0; public readonly isWarp: boolean = false; public readonly isJito: boolean = false; + public readonly isTpu: boolean = false; constructor( private readonly connection: Connection, @@ -75,6 +78,7 @@ export class Bot { ) { this.isWarp = txExecutor instanceof WarpTransactionExecutor; this.isJito = txExecutor instanceof JitoTransactionExecutor; + this.isTpu = txExecutor instanceof TpuTransactionExecutor; this.mutex = new Mutex(); this.poolFilters = new PoolFilters(connection, { @@ -150,6 +154,7 @@ export class Bot { `Send buy transaction attempt: ${i + 1}/${this.config.maxBuyRetries}`, ); const tokenOut = new Token(TOKEN_PROGRAM_ID, poolKeys.baseMint, poolKeys.baseDecimals); + console.log("detected at ", new Date().toISOString()); const result = await this.swap( poolKeys, this.config.quoteAta, @@ -160,9 +165,21 @@ export class Bot { this.config.buySlippage, this.config.wallet, 'buy', + this.config.simulateTx, ); if (result.confirmed) { + if (this.config.simulateTx) { + logger.info( + { + mint: poolState.baseMint.toString(), + signature: result.signature, + }, + `Simulated buy tx`, + ); + break; + } + logger.info( { mint: poolState.baseMint.toString(), @@ -171,7 +188,6 @@ export class Bot { }, `Confirmed buy tx`, ); - break; } @@ -246,6 +262,7 @@ export class Bot { this.config.sellSlippage, this.config.wallet, 'sell', + this.config.simulateTx, ); if (result.confirmed) { @@ -293,6 +310,7 @@ export class Bot { slippage: number, wallet: Keypair, direction: 'buy' | 'sell', + simulate: boolean ) { const slippagePercent = new Percent(slippage, 100); const poolInfo = await Liquidity.fetchInfo({ @@ -330,18 +348,18 @@ export class Bot { ...(this.isWarp || this.isJito ? [] : [ - ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }), - ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }), - ]), + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }), + ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }), + ]), ...(direction === 'buy' ? [ - createAssociatedTokenAccountIdempotentInstruction( - wallet.publicKey, - ataOut, - wallet.publicKey, - tokenOut.mint, - ), - ] + createAssociatedTokenAccountIdempotentInstruction( + wallet.publicKey, + ataOut, + wallet.publicKey, + tokenOut.mint, + ), + ] : []), ...innerTransaction.instructions, ...(direction === 'sell' ? [createCloseAccountInstruction(ataIn, wallet.publicKey, wallet.publicKey)] : []), @@ -351,7 +369,55 @@ export class Bot { const transaction = new VersionedTransaction(messageV0); transaction.sign([wallet, ...innerTransaction.signers]); - return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash); + const txResult = await this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash, simulate); + + if (txResult.confirmed) { + const historyDir = 'history'; + if (!fs.existsSync(historyDir)) + fs.mkdirSync(historyDir); + + const historyFile = `${historyDir}/${this.config.wallet.publicKey.toBase58()}.json`; + if (!fs.existsSync(historyFile)) + fs.writeFileSync(historyFile, '[]'); + const history = JSON.parse(fs.readFileSync(historyFile, 'utf8')); + + if (direction === 'buy') { + + const buyData = { + mint: tokenOut.mint.toString(), + buySignature: txResult.signature, + buyAmountIn: amountIn.toFixed(), + computedAmountOut: computedAmountOut.amountOut.toFixed(), + computedBuyPrice: Number(amountIn.toFixed()) / Number(computedAmountOut.amountOut.toFixed()), + buyTime: new Date().toISOString(), + mintBalance: '', + buyPrice: '', + }; + + if (!this.config.simulateTx) { + const tokenAccountBalance = await this.connection.getTokenAccountBalance(ataOut); + const mintBalance = new TokenAmount(tokenOut, tokenAccountBalance.value.amount, true) + buyData.mintBalance = mintBalance.toFixed(); + buyData.buyPrice = (Number(amountIn.toFixed()) / Number(mintBalance.toFixed())).toFixed(30); + } + + history.push(buyData); + } else { + const sellData = history.find((data: any) => data.mint === tokenIn.mint.toString()); + if (sellData) { + sellData.sellTx = txResult.signature; + // sellData.sellAmountIn = amountIn.toFixed(); + // sellData.computedAmountOut = computedAmountOut.amountOut.toFixed(); + // sellData.sellPrice = (Number(computedAmountOut.amountOut.toFixed()) / Number(amountIn.toFixed())).toFixed(30); + // sellData.sellTime = new Date().toISOString(); + // sellData.profit = new BN(sellData.sellPrice).sub(new BN(sellData.buyPrice)).toString(); + // sellData.profitPercent = new BN(sellData.profit).mul(new BN(100)).div(new BN(sellData.buyPrice)).toString(); + sellData.dex = `https://dexscreener.com/solana/${tokenIn.mint.toString()}?maker=${this.config.wallet.publicKey}` + } + } + fs.writeFileSync(`history/${this.config.wallet.publicKey.toBase58()}.json`, JSON.stringify(history, null, 4)); + } + return txResult; } private async filterMatch(poolKeys: LiquidityPoolKeysV4) { diff --git a/helpers/constants.ts b/helpers/constants.ts index ae50b613..058f791c 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -32,6 +32,7 @@ export const PRE_LOAD_EXISTING_MARKETS = retrieveEnvVariable('PRE_LOAD_EXISTING_ export const CACHE_NEW_MARKETS = retrieveEnvVariable('CACHE_NEW_MARKETS', logger) === 'true'; export const TRANSACTION_EXECUTOR = retrieveEnvVariable('TRANSACTION_EXECUTOR', logger); export const CUSTOM_FEE = retrieveEnvVariable('CUSTOM_FEE', logger); +export const SIMULATE_TX = retrieveEnvVariable('SIMULATE_TX', logger) === 'true'; // Buy export const AUTO_BUY_DELAY = Number(retrieveEnvVariable('AUTO_BUY_DELAY', logger)); diff --git a/index.ts b/index.ts index e4439799..2a6a1c60 100644 --- a/index.ts +++ b/index.ts @@ -45,10 +45,12 @@ import { FILTER_CHECK_INTERVAL, FILTER_CHECK_DURATION, CONSECUTIVE_FILTER_MATCHES, + SIMULATE_TX } from './helpers'; import { version } from './package.json'; import { WarpTransactionExecutor } from './transactions/warp-transaction-executor'; import { JitoTransactionExecutor } from './transactions/jito-rpc-transaction-executor'; +import { TpuTransactionExecutor } from './transactions/tpu-transaction-executor'; const connection = new Connection(RPC_ENDPOINT, { wsEndpoint: RPC_WEBSOCKET_ENDPOINT, @@ -81,8 +83,9 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) { logger.info('- Bot -'); + logger.info(`Simulate transactions: ${SIMULATE_TX}`); logger.info( - `Using ${TRANSACTION_EXECUTOR} executer: ${bot.isWarp || bot.isJito || (TRANSACTION_EXECUTOR === 'default' ? true : false)}`, + `Using ${TRANSACTION_EXECUTOR} executer: ${bot.isWarp || bot.isJito || bot.isTpu || (TRANSACTION_EXECUTOR === 'default' ? true : false)}`, ); if (bot.isWarp || bot.isJito) { logger.info(`${TRANSACTION_EXECUTOR} fee: ${CUSTOM_FEE}`); @@ -154,6 +157,10 @@ const runListener = async () => { txExecutor = new JitoTransactionExecutor(CUSTOM_FEE, connection); break; } + case 'ypu': { + txExecutor = new TpuTransactionExecutor(); + break; + } default: { txExecutor = new DefaultTransactionExecutor(connection); break; @@ -190,6 +197,7 @@ const runListener = async () => { filterCheckInterval: FILTER_CHECK_INTERVAL, filterCheckDuration: FILTER_CHECK_DURATION, consecutiveMatchCount: CONSECUTIVE_FILTER_MATCHES, + simulateTx: SIMULATE_TX }; const bot = new Bot(connection, marketCache, poolCache, txExecutor, botConfig); diff --git a/package.json b/package.json index d619d4ef..18c635d3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "bs58": "^5.0.0", "dotenv": "^16.4.1", "ed25519-hd-key": "^1.3.0", + "ffi-rs": "^1.0.69", "i": "^0.3.7", "npm": "^10.5.2", "pino": "^8.18.0", diff --git a/transactions/default-transaction-executor.ts b/transactions/default-transaction-executor.ts index 66c222ce..4d044506 100644 --- a/transactions/default-transaction-executor.ts +++ b/transactions/default-transaction-executor.ts @@ -9,27 +9,42 @@ import { TransactionExecutor } from './transaction-executor.interface'; import { logger } from '../helpers'; export class DefaultTransactionExecutor implements TransactionExecutor { - constructor(private readonly connection: Connection) {} + constructor(private readonly connection: Connection) { } public async executeAndConfirm( transaction: VersionedTransaction, payer: Keypair, latestBlockhash: BlockhashWithExpiryBlockHeight, + simulate: boolean ): Promise<{ confirmed: boolean; signature?: string }> { logger.debug('Executing transaction...'); - const signature = await this.execute(transaction); + const signature = await this.execute(transaction, payer, simulate); logger.debug({ signature }, 'Confirming transaction...'); - return this.confirm(signature, latestBlockhash); + return this.confirm(signature, latestBlockhash, simulate); } - private async execute(transaction: Transaction | VersionedTransaction) { + private async execute(transaction: Transaction | VersionedTransaction, signer?: Keypair, simulate: boolean = false) { + if (simulate) { + const simulateTx = transaction instanceof VersionedTransaction + ? await this.connection.simulateTransaction(transaction as VersionedTransaction) + : await this.connection.simulateTransaction(transaction as Transaction,[signer!]); + + logger.debug({ simulateTx }, 'Simulated transaction'); + return simulateTx.value.err ? 'ERROR' : "SUCCESS" + } + return this.connection.sendRawTransaction(transaction.serialize(), { preflightCommitment: this.connection.commitment, }); } - private async confirm(signature: string, latestBlockhash: BlockhashWithExpiryBlockHeight) { + private async confirm(signature: string, latestBlockhash: BlockhashWithExpiryBlockHeight, simulate: boolean = false) { + + if (simulate) { + return { confirmed: true, signature }; + } + const confirmation = await this.connection.confirmTransaction( { signature, diff --git a/transactions/jito-rpc-transaction-executor.ts b/transactions/jito-rpc-transaction-executor.ts index bcb8da1d..ffdf47f3 100644 --- a/transactions/jito-rpc-transaction-executor.ts +++ b/transactions/jito-rpc-transaction-executor.ts @@ -44,6 +44,7 @@ export class JitoTransactionExecutor implements TransactionExecutor { transaction: VersionedTransaction, payer: Keypair, latestBlockhash: BlockhashWithExpiryBlockHeight, + simulate: boolean ): Promise<{ confirmed: boolean; signature?: string }> { logger.debug('Starting Jito transaction execution...'); this.JitoFeeWallet = this.getRandomValidatorKey(); // Update wallet key each execution diff --git a/transactions/tpu-transaction-executor.ts b/transactions/tpu-transaction-executor.ts new file mode 100644 index 00000000..c6093b33 --- /dev/null +++ b/transactions/tpu-transaction-executor.ts @@ -0,0 +1,56 @@ +import { + BlockhashWithExpiryBlockHeight, + Keypair, + VersionedTransaction +} from '@solana/web3.js'; +import { AxiosError } from 'axios'; + +import { TransactionExecutor } from './transaction-executor.interface'; +import { load, DataType, open, close } from 'ffi-rs'; +import bs58 from 'bs58'; +import { logger } from '../helpers'; + +export class TpuTransactionExecutor implements TransactionExecutor { + + constructor() { } + + public async executeAndConfirm( + transaction: VersionedTransaction, + payer: Keypair, + latestBlockhash: BlockhashWithExpiryBlockHeight, + simulate: boolean + ): Promise<{ confirmed: boolean; signature?: string }> { + const serializedTransaction = transaction.serialize(); + const signatureBase58 = bs58.encode(transaction.signatures[0]); + let result = false + try { + open({ + library: 'tpu_client', // key + path: "/Users/kasiopea/dev/rust/tpu-sol-test/target/aarch64-apple-darwin/release/libtpu_client.dylib" // path + }) + const RPC_ENDPOINT="http://127.0.0.1:8899" + const RPC_WEBSOCKET_ENDPOINT="ws://127.0.0.1:8900" + + + result = load({ + library: "tpu_client", // path to the dynamic library file + funcName: 'send_tpu_tx', // the name of the function to call + retType: DataType.Boolean, // the return value type + paramsType: [DataType.String, DataType.String, DataType.U8Array, DataType.I32], // the parameter types + paramsValue: [RPC_ENDPOINT, RPC_WEBSOCKET_ENDPOINT, serializedTransaction, serializedTransaction.length] // the actual parameter values + }) + console.log({ + result + }) + } catch (error) { + logger.error(error, "executeAndConfirm"); + if (error instanceof AxiosError) { + logger.trace({ error: error.response?.data }, 'Failed to execute warp transaction'); + } + } finally { + close('tpu_client') + } + + return { confirmed: result, signature: signatureBase58 }; + } +} diff --git a/transactions/transaction-executor.interface.ts b/transactions/transaction-executor.interface.ts index 2ff7044c..1b8cdeff 100644 --- a/transactions/transaction-executor.interface.ts +++ b/transactions/transaction-executor.interface.ts @@ -5,5 +5,6 @@ export interface TransactionExecutor { transaction: VersionedTransaction, payer: Keypair, latestBlockHash: BlockhashWithExpiryBlockHeight, + simulate: boolean, ): Promise<{ confirmed: boolean; signature?: string, error?: string }>; } diff --git a/transactions/warp-transaction-executor.ts b/transactions/warp-transaction-executor.ts index da4245fa..993c01b3 100644 --- a/transactions/warp-transaction-executor.ts +++ b/transactions/warp-transaction-executor.ts @@ -15,12 +15,13 @@ import { Currency, CurrencyAmount } from '@raydium-io/raydium-sdk'; export class WarpTransactionExecutor implements TransactionExecutor { private readonly warpFeeWallet = new PublicKey('WARPzUMPnycu9eeCZ95rcAUxorqpBqHndfV3ZP5FSyS'); - constructor(private readonly warpFee: string) {} + constructor(private readonly warpFee: string) { } public async executeAndConfirm( transaction: VersionedTransaction, payer: Keypair, latestBlockhash: BlockhashWithExpiryBlockHeight, + simulate: boolean ): Promise<{ confirmed: boolean; signature?: string }> { logger.debug('Executing transaction...');