mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-10 04:22:05 +10:00
feat: remove delay and listen to wallet changes
This commit is contained in:
@ -2,10 +2,10 @@ PRIVATE_KEY=
|
||||
RPC_ENDPOINT=https://api.mainnet-beta.solana.com
|
||||
RPC_WEBSOCKET_ENDPOINT=wss://api.mainnet-beta.solana.com
|
||||
QUOTE_MINT=WSOL
|
||||
QUOTE_AMOUNT=0.1
|
||||
QUOTE_AMOUNT=0.01
|
||||
COMMITMENT_LEVEL=finalized
|
||||
USE_SNIPE_LIST=false
|
||||
SNIPE_LIST_REFRESH_INTERVAL=30000
|
||||
CHECK_IF_MINT_IS_RENOUNCED=false
|
||||
AUTO_SELL=false
|
||||
SELL_DELAY=2000
|
||||
AUTO_SELL=true
|
||||
MAX_SELL_RETRIES=5
|
||||
14
README.md
14
README.md
@ -42,13 +42,11 @@ Pool must not exist before the script starts.
|
||||
It will buy only when new pool is open for trading. If you want to buy token that will be launched in the future, make sure that script is running before the launch.
|
||||
|
||||
## Auto Sell
|
||||
By default, auto sell is disabled. If you want to enable it, you need to:
|
||||
- Change variable `AUTO_SELL` to `true`
|
||||
- Update `SELL_DELAY` to the number of milliseconds you want to wait before selling the token
|
||||
By default, auto sell is enabled. If you want to disable it, you need to:
|
||||
- Change variable `AUTO_SELL` to `false`
|
||||
- Update `MAX_SELL_RETRIES` to set the maximum number of retries for selling token
|
||||
|
||||
This will sell the token after the specified delay. (+- RPC node speed)
|
||||
|
||||
This feature is **experimental** and should be used with caution. Make sure you understand the risks before enabling it. There is no guarantee that the token will be sold at a profit or even sold at all. The developer is not responsible for any losses incurred by using this feature.
|
||||
Token will be sold immediately after it is bought.
|
||||
|
||||
## Common issues
|
||||
If you have an error which is not listed here, please create a new issue in this repository.
|
||||
@ -72,3 +70,7 @@ If you have an error which is not listed here, please create a new issue in this
|
||||
|
||||
## Contact
|
||||
[](https://discord.gg/xYUETCA2aP)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Use this script at your own risk.
|
||||
|
||||
331
buy.ts
331
buy.ts
@ -1,4 +1,5 @@
|
||||
import {
|
||||
BigNumberish,
|
||||
Liquidity,
|
||||
LIQUIDITY_STATE_LAYOUT_V4,
|
||||
LiquidityPoolKeys,
|
||||
@ -9,6 +10,7 @@ import {
|
||||
TokenAmount,
|
||||
} from '@raydium-io/raydium-sdk';
|
||||
import {
|
||||
AccountLayout,
|
||||
createAssociatedTokenAccountIdempotentInstruction,
|
||||
createCloseAccountInstruction,
|
||||
getAssociatedTokenAddressSync,
|
||||
@ -32,7 +34,6 @@ import pino from 'pino';
|
||||
import bs58 from 'bs58';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import BN from 'bn.js';
|
||||
|
||||
const transport = pino.transport({
|
||||
targets: [
|
||||
@ -45,7 +46,7 @@ const transport = pino.transport({
|
||||
// },
|
||||
|
||||
{
|
||||
level: 'trace',
|
||||
level: 'info',
|
||||
target: 'pino-pretty',
|
||||
options: {},
|
||||
},
|
||||
@ -54,6 +55,7 @@ const transport = pino.transport({
|
||||
|
||||
export const logger = pino(
|
||||
{
|
||||
level: 'info',
|
||||
redact: ['poolKeys'],
|
||||
serializers: {
|
||||
error: pino.stdSerializers.err,
|
||||
@ -92,8 +94,7 @@ const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNC
|
||||
const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
|
||||
const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
|
||||
const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
|
||||
const SELL_DELAY = Number(retrieveEnvVariable('SELL_DELAY', logger));
|
||||
const MAX_SELL_RETRIES = 60;
|
||||
const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
|
||||
|
||||
let snipeList: string[] = [];
|
||||
|
||||
@ -170,30 +171,20 @@ function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) {
|
||||
}
|
||||
|
||||
export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) {
|
||||
try {
|
||||
if (!shouldBuy(poolState.baseMint.toString())) {
|
||||
if (!shouldBuy(poolState.baseMint.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CHECK_IF_MINT_IS_RENOUNCED) {
|
||||
const mintOption = await checkMintable(poolState.baseMint);
|
||||
|
||||
if (mintOption !== true) {
|
||||
logger.warn({ mint: poolState.baseMint }, 'Skipping, owner can mint tokens!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (CHECK_IF_MINT_IS_RENOUNCED) {
|
||||
const mintOption = await checkMintable(poolState.baseMint);
|
||||
|
||||
if (mintOption !== true) {
|
||||
logger.warn({ ...poolState, }, 'Skipping, owner can mint tokens!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await buy(id, poolState);
|
||||
|
||||
if (AUTO_SELL) {
|
||||
await new Promise((resolve) => setTimeout(resolve, SELL_DELAY));
|
||||
const poolKeys = existingTokenAccounts.get(poolState.baseMint.toString())!.poolKeys;
|
||||
await sell(poolState, poolKeys as LiquidityPoolKeys);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error({ ...poolState, error: e }, `Failed to process pool`);
|
||||
}
|
||||
|
||||
await buy(id, poolState);
|
||||
}
|
||||
|
||||
export async function checkMintable(vault: PublicKey): Promise<boolean | undefined> {
|
||||
@ -202,16 +193,15 @@ export async function checkMintable(vault: PublicKey): Promise<boolean | undefin
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const deserialize = MintLayout.decode(data), mintAuthorityOption = deserialize.mintAuthorityOption;
|
||||
return mintAuthorityOption === 0;
|
||||
const deserialize = MintLayout.decode(data);
|
||||
return deserialize.mintAuthorityOption === 0;
|
||||
} catch (e) {
|
||||
logger.error({ mint: vault, error: e }, `Failed to check if mint is renounced`);
|
||||
logger.debug(e);
|
||||
logger.error({ mint: vault }, `Failed to check if mint is renounced`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function processOpenBookMarket(
|
||||
updatedAccountInfo: KeyedAccountInfo,
|
||||
) {
|
||||
export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) {
|
||||
let accountData: MarketStateV3 | undefined;
|
||||
try {
|
||||
accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
|
||||
@ -223,132 +213,172 @@ export async function processOpenBookMarket(
|
||||
|
||||
saveTokenAccount(accountData.baseMint, accountData);
|
||||
} catch (e) {
|
||||
logger.error({ ...accountData, error: e }, `Failed to process market`);
|
||||
logger.debug(e);
|
||||
logger.error({ mint: accountData?.baseMint }, `Failed to process market`);
|
||||
}
|
||||
}
|
||||
|
||||
async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise<void> {
|
||||
let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
|
||||
try {
|
||||
let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
|
||||
|
||||
if (!tokenAccount) {
|
||||
// it's possible that we didn't have time to fetch open book data
|
||||
const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
|
||||
tokenAccount = saveTokenAccount(accountData.baseMint, market);
|
||||
}
|
||||
if (!tokenAccount) {
|
||||
// it's possible that we didn't have time to fetch open book data
|
||||
const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
|
||||
tokenAccount = saveTokenAccount(accountData.baseMint, market);
|
||||
}
|
||||
|
||||
tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
|
||||
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
|
||||
{
|
||||
poolKeys: tokenAccount.poolKeys,
|
||||
userKeys: {
|
||||
tokenAccountIn: quoteTokenAssociatedAddress,
|
||||
tokenAccountOut: tokenAccount.address,
|
||||
owner: wallet.publicKey,
|
||||
tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
|
||||
const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
|
||||
{
|
||||
poolKeys: tokenAccount.poolKeys,
|
||||
userKeys: {
|
||||
tokenAccountIn: quoteTokenAssociatedAddress,
|
||||
tokenAccountOut: tokenAccount.address,
|
||||
owner: wallet.publicKey,
|
||||
},
|
||||
amountIn: quoteAmount.raw,
|
||||
minAmountOut: 0,
|
||||
},
|
||||
amountIn: quoteAmount.raw,
|
||||
minAmountOut: 0,
|
||||
},
|
||||
tokenAccount.poolKeys.version,
|
||||
);
|
||||
tokenAccount.poolKeys.version,
|
||||
);
|
||||
|
||||
const latestBlockhash = await solanaConnection.getLatestBlockhash({
|
||||
commitment: commitment,
|
||||
});
|
||||
const messageV0 = new TransactionMessage({
|
||||
payerKey: wallet.publicKey,
|
||||
recentBlockhash: latestBlockhash.blockhash,
|
||||
instructions: [
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
|
||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }),
|
||||
createAssociatedTokenAccountIdempotentInstruction(
|
||||
wallet.publicKey,
|
||||
tokenAccount.address,
|
||||
wallet.publicKey,
|
||||
accountData.baseMint,
|
||||
),
|
||||
...innerTransaction.instructions,
|
||||
],
|
||||
}).compileToV0Message();
|
||||
const transaction = new VersionedTransaction(messageV0);
|
||||
transaction.sign([wallet, ...innerTransaction.signers]);
|
||||
const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
|
||||
maxRetries: 20,
|
||||
preflightCommitment: commitment,
|
||||
});
|
||||
logger.info(
|
||||
{
|
||||
mint: accountData.baseMint,
|
||||
url: `https://solscan.io/tx/${signature}?cluster=${network}`,
|
||||
dexURL: `https://dexscreener.com/solana/${accountData.baseMint}?maker=${wallet.publicKey}`,
|
||||
},
|
||||
'Buy',
|
||||
);
|
||||
const latestBlockhash = await solanaConnection.getLatestBlockhash({
|
||||
commitment: commitment,
|
||||
});
|
||||
const messageV0 = new TransactionMessage({
|
||||
payerKey: wallet.publicKey,
|
||||
recentBlockhash: latestBlockhash.blockhash,
|
||||
instructions: [
|
||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }),
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }),
|
||||
createAssociatedTokenAccountIdempotentInstruction(
|
||||
wallet.publicKey,
|
||||
tokenAccount.address,
|
||||
wallet.publicKey,
|
||||
accountData.baseMint,
|
||||
),
|
||||
...innerTransaction.instructions,
|
||||
],
|
||||
}).compileToV0Message();
|
||||
const transaction = new VersionedTransaction(messageV0);
|
||||
transaction.sign([wallet, ...innerTransaction.signers]);
|
||||
const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
|
||||
preflightCommitment: commitment,
|
||||
});
|
||||
logger.info({ mint: accountData.baseMint, signature }, `Sent buy tx`);
|
||||
const confirmation = await solanaConnection.confirmTransaction(
|
||||
{
|
||||
signature,
|
||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
},
|
||||
commitment,
|
||||
);
|
||||
if (!confirmation.value.err) {
|
||||
logger.info(
|
||||
{
|
||||
mint: accountData.baseMint,
|
||||
signature,
|
||||
url: `https://solscan.io/tx/${signature}?cluster=${network}`,
|
||||
},
|
||||
`Confirmed buy tx`,
|
||||
);
|
||||
} else {
|
||||
logger.debug(confirmation.value.err);
|
||||
logger.info({ mint: accountData.baseMint, signature }, `Error confirming buy tx`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug(e);
|
||||
logger.error({ mint: accountData.baseMint }, `Failed to buy token`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sell(accountData: LiquidityStateV4, poolKeys: LiquidityPoolKeys): Promise<void> {
|
||||
const tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
|
||||
|
||||
if (!tokenAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function sell(accountId: PublicKey, mint: PublicKey, amount: BigNumberish): Promise<void> {
|
||||
let sold = false;
|
||||
let retries = 0;
|
||||
let balanceFound = false;
|
||||
while (retries < MAX_SELL_RETRIES) {
|
||||
|
||||
do {
|
||||
try {
|
||||
const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount;
|
||||
const tokenAccount = existingTokenAccounts.get(mint.toString());
|
||||
|
||||
if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) {
|
||||
balanceFound = true;
|
||||
if (!tokenAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
|
||||
{
|
||||
poolKeys: poolKeys,
|
||||
userKeys: {
|
||||
tokenAccountIn: tokenAccount.address,
|
||||
tokenAccountOut: quoteTokenAssociatedAddress,
|
||||
owner: wallet.publicKey,
|
||||
},
|
||||
amountIn: new BN(balanceResponse),
|
||||
minAmountOut: 0,
|
||||
},
|
||||
poolKeys.version,
|
||||
);
|
||||
if (!tokenAccount.poolKeys) {
|
||||
logger.warn({ mint }, 'No pool keys found');
|
||||
continue;
|
||||
}
|
||||
|
||||
const latestBlockhash = await solanaConnection.getLatestBlockhash({
|
||||
commitment: commitment,
|
||||
});
|
||||
const messageV0 = new TransactionMessage({
|
||||
payerKey: wallet.publicKey,
|
||||
recentBlockhash: latestBlockhash.blockhash,
|
||||
instructions: [
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
|
||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 200000 }),
|
||||
...innerTransaction.instructions,
|
||||
createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
|
||||
],
|
||||
}).compileToV0Message();
|
||||
const transaction = new VersionedTransaction(messageV0);
|
||||
transaction.sign([wallet, ...innerTransaction.signers]);
|
||||
const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
|
||||
maxRetries: 5,
|
||||
preflightCommitment: commitment,
|
||||
});
|
||||
if (amount === 0) {
|
||||
logger.info(
|
||||
{
|
||||
mint: accountData.baseMint,
|
||||
url: `https://solscan.io/tx/${signature}?cluster=${network}`,
|
||||
mint: tokenAccount.mint,
|
||||
},
|
||||
'sell',
|
||||
`Empty balance, can't sell`,
|
||||
);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// ignored
|
||||
|
||||
const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
|
||||
{
|
||||
poolKeys: tokenAccount.poolKeys!,
|
||||
userKeys: {
|
||||
tokenAccountOut: quoteTokenAssociatedAddress,
|
||||
tokenAccountIn: tokenAccount.address,
|
||||
owner: wallet.publicKey,
|
||||
},
|
||||
amountIn: amount,
|
||||
minAmountOut: 0,
|
||||
},
|
||||
tokenAccount.poolKeys!.version,
|
||||
);
|
||||
|
||||
const latestBlockhash = await solanaConnection.getLatestBlockhash({
|
||||
commitment: commitment,
|
||||
});
|
||||
const messageV0 = new TransactionMessage({
|
||||
payerKey: wallet.publicKey,
|
||||
recentBlockhash: latestBlockhash.blockhash,
|
||||
instructions: [
|
||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 421197 }),
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 101337 }),
|
||||
...innerTransaction.instructions,
|
||||
createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
|
||||
],
|
||||
}).compileToV0Message();
|
||||
const transaction = new VersionedTransaction(messageV0);
|
||||
transaction.sign([wallet, ...innerTransaction.signers]);
|
||||
const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
|
||||
preflightCommitment: commitment,
|
||||
});
|
||||
logger.info({ mint, signature }, `Sent sell tx`);
|
||||
const confirmation = await solanaConnection.confirmTransaction(
|
||||
{
|
||||
signature,
|
||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
},
|
||||
commitment,
|
||||
);
|
||||
if (confirmation.value.err) {
|
||||
logger.debug(confirmation.value.err);
|
||||
logger.info({ mint, signature }, `Error confirming sell tx`);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ mint, signature, url: `https://solscan.io/tx/${signature}?cluster=${network}` },
|
||||
`Confirmed sell tx`,
|
||||
);
|
||||
sold = true;
|
||||
} catch (e: any) {
|
||||
retries++;
|
||||
logger.debug(e);
|
||||
logger.error({ mint }, `Failed to sell token, retry: ${retries}/${MAX_SELL_RETRIES}`);
|
||||
}
|
||||
retries++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
} while (!sold && retries < MAX_SELL_RETRIES);
|
||||
}
|
||||
|
||||
function loadSnipeList() {
|
||||
@ -434,6 +464,35 @@ const runListener = async () => {
|
||||
],
|
||||
);
|
||||
|
||||
if (AUTO_SELL) {
|
||||
const walletSubscriptionId = solanaConnection.onProgramAccountChange(
|
||||
TOKEN_PROGRAM_ID,
|
||||
async (updatedAccountInfo) => {
|
||||
const accountData = AccountLayout.decode(updatedAccountInfo.accountInfo!.data);
|
||||
|
||||
if (updatedAccountInfo.accountId.equals(quoteTokenAssociatedAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _ = sell(updatedAccountInfo.accountId, accountData.mint, accountData.amount);
|
||||
},
|
||||
commitment,
|
||||
[
|
||||
{
|
||||
dataSize: 165,
|
||||
},
|
||||
{
|
||||
memcmp: {
|
||||
offset: 32,
|
||||
bytes: wallet.publicKey.toBase58(),
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
logger.info(`Listening for wallet changes: ${walletSubscriptionId}`);
|
||||
}
|
||||
|
||||
logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
|
||||
logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Logger } from "pino";
|
||||
import { Logger } from 'pino';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
|
||||
@ -9,4 +10,4 @@ export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
|
||||
process.exit(1);
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user