mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-19 03:01:19 +10:00
@ -6,3 +6,5 @@ QUOTE_AMOUNT=0.1
|
|||||||
COMMITMENT_LEVEL=finalized
|
COMMITMENT_LEVEL=finalized
|
||||||
USE_SNIPE_LIST=false
|
USE_SNIPE_LIST=false
|
||||||
SNIPE_LIST_REFRESH_INTERVAL=30000
|
SNIPE_LIST_REFRESH_INTERVAL=30000
|
||||||
|
AUTO_SELL=false
|
||||||
|
SELL_DELAY=2000
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -129,5 +129,5 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
# JetBrains
|
||||||
.idea
|
.idea
|
||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
@ -40,6 +40,15 @@ You can update the list while script is running. Script will check for new value
|
|||||||
Pool must not exist before the script starts.
|
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.
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Common issues
|
## Common issues
|
||||||
If you have an error which is not listed here, please create a new issue in this repository.
|
If you have an error which is not listed here, please create a new issue in this repository.
|
||||||
|
|
||||||
|
|||||||
184
buy.ts
184
buy.ts
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@raydium-io/raydium-sdk';
|
} from '@raydium-io/raydium-sdk';
|
||||||
import {
|
import {
|
||||||
createAssociatedTokenAccountIdempotentInstruction,
|
createAssociatedTokenAccountIdempotentInstruction,
|
||||||
|
createCloseAccountInstruction,
|
||||||
getAssociatedTokenAddressSync,
|
getAssociatedTokenAddressSync,
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
} from '@solana/spl-token';
|
} from '@solana/spl-token';
|
||||||
@ -23,30 +24,25 @@ import {
|
|||||||
VersionedTransaction,
|
VersionedTransaction,
|
||||||
Commitment,
|
Commitment,
|
||||||
} from '@solana/web3.js';
|
} from '@solana/web3.js';
|
||||||
import {
|
import { getTokenAccounts, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, OPENBOOK_PROGRAM_ID, createPoolKeys } from './liquidity';
|
||||||
getTokenAccounts,
|
|
||||||
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
|
||||||
OPENBOOK_PROGRAM_ID,
|
|
||||||
createPoolKeys,
|
|
||||||
} from './liquidity';
|
|
||||||
import { retrieveEnvVariable } from './utils';
|
import { retrieveEnvVariable } from './utils';
|
||||||
import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market';
|
import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import bs58 from 'bs58';
|
import bs58 from 'bs58';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import BN from 'bn.js';
|
||||||
|
|
||||||
const transport = pino.transport({
|
const transport = pino.transport({
|
||||||
targets: [
|
targets: [
|
||||||
/*
|
// {
|
||||||
{
|
// level: 'trace',
|
||||||
level: 'trace',
|
// target: 'pino/file',
|
||||||
target: 'pino/file',
|
// options: {
|
||||||
options: {
|
// destination: 'buy.log',
|
||||||
destination: 'buy.log',
|
// },
|
||||||
},
|
// },
|
||||||
},
|
|
||||||
*/
|
|
||||||
{
|
{
|
||||||
level: 'trace',
|
level: 'trace',
|
||||||
target: 'pino-pretty',
|
target: 'pino-pretty',
|
||||||
@ -68,10 +64,7 @@ export const logger = pino(
|
|||||||
|
|
||||||
const network = 'mainnet-beta';
|
const network = 'mainnet-beta';
|
||||||
const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
|
const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
|
||||||
const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable(
|
const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
|
||||||
'RPC_WEBSOCKET_ENDPOINT',
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
const solanaConnection = new Connection(RPC_ENDPOINT, {
|
const solanaConnection = new Connection(RPC_ENDPOINT, {
|
||||||
wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
|
wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
|
||||||
@ -86,24 +79,20 @@ export type MinimalTokenAccountData = {
|
|||||||
|
|
||||||
let existingLiquidityPools: Set<string> = new Set<string>();
|
let existingLiquidityPools: Set<string> = new Set<string>();
|
||||||
let existingOpenBookMarkets: Set<string> = new Set<string>();
|
let existingOpenBookMarkets: Set<string> = new Set<string>();
|
||||||
let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<
|
let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<string, MinimalTokenAccountData>();
|
||||||
string,
|
|
||||||
MinimalTokenAccountData
|
|
||||||
>();
|
|
||||||
|
|
||||||
let wallet: Keypair;
|
let wallet: Keypair;
|
||||||
let quoteToken: Token;
|
let quoteToken: Token;
|
||||||
let quoteTokenAssociatedAddress: PublicKey;
|
let quoteTokenAssociatedAddress: PublicKey;
|
||||||
let quoteAmount: TokenAmount;
|
let quoteAmount: TokenAmount;
|
||||||
let commitment: Commitment = retrieveEnvVariable(
|
let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
|
||||||
'COMMITMENT_LEVEL',
|
|
||||||
logger,
|
|
||||||
) as Commitment;
|
|
||||||
|
|
||||||
const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
|
const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
|
||||||
const SNIPE_LIST_REFRESH_INTERVAL = Number(
|
const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
|
||||||
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;
|
||||||
|
|
||||||
let snipeList: string[] = [];
|
let snipeList: string[] = [];
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
@ -133,9 +122,7 @@ async function init(): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`);
|
||||||
`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,29 +131,19 @@ async function init(): Promise<void> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// check existing wallet for associated token account of quote mint
|
// check existing wallet for associated token account of quote mint
|
||||||
const tokenAccounts = await getTokenAccounts(
|
const tokenAccounts = await getTokenAccounts(solanaConnection, wallet.publicKey, commitment);
|
||||||
solanaConnection,
|
|
||||||
wallet.publicKey,
|
|
||||||
commitment,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const ta of tokenAccounts) {
|
for (const ta of tokenAccounts) {
|
||||||
existingTokenAccounts.set(ta.accountInfo.mint.toString(), <
|
existingTokenAccounts.set(ta.accountInfo.mint.toString(), <MinimalTokenAccountData>{
|
||||||
MinimalTokenAccountData
|
|
||||||
>{
|
|
||||||
mint: ta.accountInfo.mint,
|
mint: ta.accountInfo.mint,
|
||||||
address: ta.pubkey,
|
address: ta.pubkey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenAccount = tokenAccounts.find(
|
const tokenAccount = tokenAccounts.find((acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString())!;
|
||||||
(acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString(),
|
|
||||||
)!;
|
|
||||||
|
|
||||||
if (!tokenAccount) {
|
if (!tokenAccount) {
|
||||||
throw new Error(
|
throw new Error(`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`);
|
||||||
`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteTokenAssociatedAddress = tokenAccount.pubkey;
|
quoteTokenAssociatedAddress = tokenAccount.pubkey;
|
||||||
@ -190,29 +167,28 @@ function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) {
|
|||||||
return tokenAccount;
|
return tokenAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processRaydiumPool(
|
export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) {
|
||||||
id: PublicKey,
|
|
||||||
poolState: LiquidityStateV4,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (!shouldBuy(poolState.baseMint.toString())) {
|
if (!shouldBuy(poolState.baseMint.toString())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await buy(id, poolState);
|
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) {
|
} catch (e) {
|
||||||
logger.error({ ...poolState, error: e }, `Failed to process pool`);
|
logger.error({ ...poolState, error: e }, `Failed to process pool`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processOpenBookMarket(
|
export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) {
|
||||||
updatedAccountInfo: KeyedAccountInfo,
|
|
||||||
) {
|
|
||||||
let accountData: MarketStateV3 | undefined;
|
let accountData: MarketStateV3 | undefined;
|
||||||
try {
|
try {
|
||||||
accountData = MARKET_STATE_LAYOUT_V3.decode(
|
accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
|
||||||
updatedAccountInfo.accountInfo.data,
|
|
||||||
);
|
|
||||||
|
|
||||||
// to be competitive, we collect market data before buying the token...
|
// to be competitive, we collect market data before buying the token...
|
||||||
if (existingTokenAccounts.has(accountData.baseMint.toString())) {
|
if (existingTokenAccounts.has(accountData.baseMint.toString())) {
|
||||||
@ -225,27 +201,16 @@ export async function processOpenBookMarket(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buy(
|
async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise<void> {
|
||||||
accountId: PublicKey,
|
|
||||||
accountData: LiquidityStateV4,
|
|
||||||
): Promise<void> {
|
|
||||||
let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
|
let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
|
||||||
|
|
||||||
if (!tokenAccount) {
|
if (!tokenAccount) {
|
||||||
// it's possible that we didn't have time to fetch open book data
|
// it's possible that we didn't have time to fetch open book data
|
||||||
const market = await getMinimalMarketV3(
|
const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
|
||||||
solanaConnection,
|
|
||||||
accountData.marketId,
|
|
||||||
commitment,
|
|
||||||
);
|
|
||||||
tokenAccount = saveTokenAccount(accountData.baseMint, market);
|
tokenAccount = saveTokenAccount(accountData.baseMint, market);
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenAccount.poolKeys = createPoolKeys(
|
tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
|
||||||
accountId,
|
|
||||||
accountData,
|
|
||||||
tokenAccount.market!,
|
|
||||||
);
|
|
||||||
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
|
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
|
||||||
{
|
{
|
||||||
poolKeys: tokenAccount.poolKeys,
|
poolKeys: tokenAccount.poolKeys,
|
||||||
@ -280,13 +245,10 @@ async function buy(
|
|||||||
}).compileToV0Message();
|
}).compileToV0Message();
|
||||||
const transaction = new VersionedTransaction(messageV0);
|
const transaction = new VersionedTransaction(messageV0);
|
||||||
transaction.sign([wallet, ...innerTransaction.signers]);
|
transaction.sign([wallet, ...innerTransaction.signers]);
|
||||||
const signature = await solanaConnection.sendRawTransaction(
|
const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
|
||||||
transaction.serialize(),
|
|
||||||
{
|
|
||||||
maxRetries: 20,
|
maxRetries: 20,
|
||||||
preflightCommitment: commitment,
|
preflightCommitment: commitment,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
mint: accountData.baseMint,
|
mint: accountData.baseMint,
|
||||||
@ -296,6 +258,72 @@ async function buy(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sell(accountData: LiquidityStateV4, poolKeys: LiquidityPoolKeys): Promise<void> {
|
||||||
|
const tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
|
||||||
|
|
||||||
|
if (!tokenAccount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let retries = 0;
|
||||||
|
let balanceFound = false;
|
||||||
|
while (retries < MAX_SELL_RETRIES) {
|
||||||
|
try {
|
||||||
|
const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount;
|
||||||
|
|
||||||
|
if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) {
|
||||||
|
balanceFound = true;
|
||||||
|
|
||||||
|
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
|
||||||
|
{
|
||||||
|
poolKeys: poolKeys,
|
||||||
|
userKeys: {
|
||||||
|
tokenAccountIn: tokenAccount.address,
|
||||||
|
tokenAccountOut: quoteTokenAssociatedAddress,
|
||||||
|
owner: wallet.publicKey,
|
||||||
|
},
|
||||||
|
amountIn: new BN(balanceResponse),
|
||||||
|
minAmountOut: 0,
|
||||||
|
},
|
||||||
|
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: 200000 }),
|
||||||
|
createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
|
||||||
|
...innerTransaction.instructions,
|
||||||
|
],
|
||||||
|
}).compileToV0Message();
|
||||||
|
const transaction = new VersionedTransaction(messageV0);
|
||||||
|
transaction.sign([wallet, ...innerTransaction.signers]);
|
||||||
|
const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
|
||||||
|
maxRetries: 5,
|
||||||
|
preflightCommitment: commitment,
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
mint: accountData.baseMint,
|
||||||
|
url: `https://solscan.io/tx/${signature}?cluster=${network}`,
|
||||||
|
},
|
||||||
|
'sell',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
retries++;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadSnipeList() {
|
function loadSnipeList() {
|
||||||
if (!USE_SNIPE_LIST) {
|
if (!USE_SNIPE_LIST) {
|
||||||
return;
|
return;
|
||||||
@ -324,9 +352,7 @@ const runListener = async () => {
|
|||||||
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
||||||
async (updatedAccountInfo) => {
|
async (updatedAccountInfo) => {
|
||||||
const key = updatedAccountInfo.accountId.toString();
|
const key = updatedAccountInfo.accountId.toString();
|
||||||
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(
|
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
|
||||||
updatedAccountInfo.accountInfo.data,
|
|
||||||
);
|
|
||||||
const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
|
const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
|
||||||
const existing = existingLiquidityPools.has(key);
|
const existing = existingLiquidityPools.has(key);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user