mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-09 20:12:06 +10:00
fix: cleanup sell function
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
136
buy.ts
136
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,12 +24,7 @@ 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';
|
||||||
@ -39,7 +35,6 @@ 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',
|
||||||
@ -69,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,
|
||||||
@ -87,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> {
|
||||||
@ -134,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`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,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;
|
||||||
@ -191,10 +167,7 @@ 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;
|
||||||
@ -202,30 +175,20 @@ export async function processRaydiumPool(
|
|||||||
|
|
||||||
await buy(id, poolState);
|
await buy(id, poolState);
|
||||||
|
|
||||||
|
if (AUTO_SELL) {
|
||||||
const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger);
|
await new Promise((resolve) => setTimeout(resolve, SELL_DELAY));
|
||||||
if (AUTO_SELL === 'true') {
|
const poolKeys = existingTokenAccounts.get(poolState.baseMint.toString())!.poolKeys;
|
||||||
// wait for a bit before selling
|
await sell(poolState, poolKeys as LiquidityPoolKeys);
|
||||||
const SELL_DELAY = retrieveEnvVariable('SELL_DELAY', logger);
|
|
||||||
const timeout = parseInt(SELL_DELAY, 10);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
||||||
|
|
||||||
let poolKeys = existingTokenAccounts.get(poolState.baseMint.toString())!.poolKeys;
|
|
||||||
await sell(id, 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())) {
|
||||||
@ -238,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,
|
||||||
@ -293,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,
|
||||||
@ -309,24 +258,19 @@ async function buy(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxRetries = 60;
|
async function sell(accountData: LiquidityStateV4, poolKeys: LiquidityPoolKeys): Promise<void> {
|
||||||
async function sell(
|
const tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
|
||||||
accountId: PublicKey,
|
|
||||||
accountData: LiquidityStateV4,
|
|
||||||
poolKeys: LiquidityPoolKeys,
|
|
||||||
): Promise<void> {
|
|
||||||
const tokenAccount = existingTokenAccounts.get(
|
|
||||||
accountData.baseMint.toString(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tokenAccount) {
|
if (!tokenAccount) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
let balanceFound = false;
|
let balanceFound = false;
|
||||||
while (retries < maxRetries) {
|
while (retries < MAX_SELL_RETRIES) {
|
||||||
try {
|
try {
|
||||||
const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount;
|
const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount;
|
||||||
|
|
||||||
if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) {
|
if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) {
|
||||||
balanceFound = true;
|
balanceFound = true;
|
||||||
|
|
||||||
@ -353,18 +297,16 @@ async function sell(
|
|||||||
instructions: [
|
instructions: [
|
||||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
|
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
|
||||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 200000 }),
|
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 200000 }),
|
||||||
|
createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
|
||||||
...innerTransaction.instructions,
|
...innerTransaction.instructions,
|
||||||
],
|
],
|
||||||
}).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: 5,
|
maxRetries: 5,
|
||||||
preflightCommitment: commitment,
|
preflightCommitment: commitment,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
mint: accountData.baseMint,
|
mint: accountData.baseMint,
|
||||||
@ -375,14 +317,13 @@ async function sell(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// logger.error(`Error while selling: ${error}`);
|
// ignored
|
||||||
}
|
}
|
||||||
retries++;
|
retries++;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadSnipeList() {
|
function loadSnipeList() {
|
||||||
if (!USE_SNIPE_LIST) {
|
if (!USE_SNIPE_LIST) {
|
||||||
return;
|
return;
|
||||||
@ -411,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);
|
||||||
|
|
||||||
@ -471,7 +410,6 @@ const runListener = async () => {
|
|||||||
logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
|
logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
|
||||||
logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
|
logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
|
||||||
|
|
||||||
|
|
||||||
if (USE_SNIPE_LIST) {
|
if (USE_SNIPE_LIST) {
|
||||||
setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
|
setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user