mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-09 20:12:06 +10:00
Add auto sell feature
This commit is contained in:
60
buy.ts
60
buy.ts
@ -201,6 +201,22 @@ export async function processRaydiumPool(
|
||||
}
|
||||
|
||||
await buy(id, poolState);
|
||||
|
||||
|
||||
const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger);
|
||||
if (AUTO_SELL === 'true') {
|
||||
// wait for a bit before selling
|
||||
const SELL_DELAY = retrieveEnvVariable('SELL_DELAY', logger);
|
||||
const timeout = parseInt(SELL_DELAY, 10);
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
|
||||
// log poolstate info
|
||||
// logger.info({ poolState }, `Pool state info`);
|
||||
|
||||
await sell(id, poolState);
|
||||
}
|
||||
|
||||
// await sell(id, poolState);
|
||||
} catch (e) {
|
||||
logger.error({ ...poolState, error: e }, `Failed to process pool`);
|
||||
}
|
||||
@ -531,17 +547,23 @@ const runListener = async () => {
|
||||
logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
|
||||
logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
|
||||
|
||||
// post to discord webhook
|
||||
|
||||
if (USE_SNIPE_LIST) {
|
||||
setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
// runListener();
|
||||
|
||||
// make sure we can send a message on discord if there is an error or the script exits
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error(reason, 'Unhandled Rejection at:', promise);
|
||||
const message = {
|
||||
embeds: [
|
||||
{
|
||||
title: `Listening for raydium changes: ${raydiumSubscriptionId}`,
|
||||
title: `Unhandled Rejection: ${reason}`,
|
||||
color: 1127128,
|
||||
},
|
||||
{
|
||||
title: `Listening for open book changes: ${openBookSubscriptionId}`,
|
||||
color: 14177041,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -554,10 +576,28 @@ const runListener = async () => {
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
});
|
||||
|
||||
if (USE_SNIPE_LIST) {
|
||||
setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
|
||||
}
|
||||
};
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error(err, 'Uncaught Exception thrown');
|
||||
const message = {
|
||||
embeds: [
|
||||
{
|
||||
title: `Uncaught Exception: ${err}`,
|
||||
color: 1127128,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const DISCORD_WEBHOOK = retrieveEnvVariable('DISCORD_WEBHOOK', logger);
|
||||
// use native fetch to post to discord
|
||||
fetch(DISCORD_WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
});
|
||||
|
||||
runListener();
|
||||
@ -2,7 +2,8 @@
|
||||
"name": "solana-sniper-bot",
|
||||
"author": "Filip Dundjer",
|
||||
"scripts": {
|
||||
"buy": "ts-node buy.ts"
|
||||
"buy": "ts-node buy.ts",
|
||||
"work": "ts-node worker/watcher.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@raydium-io/raydium-sdk": "^1.3.1-beta.47",
|
||||
@ -22,4 +23,4 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@ -9,8 +9,11 @@ dependencies:
|
||||
specifier: ^1.3.1-beta.47
|
||||
version: 1.3.1-beta.47(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
|
||||
'@solana/spl-token':
|
||||
specifier: ^0.3.11
|
||||
version: 0.3.11(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
|
||||
'@solana/web3.js':
|
||||
specifier: ^1.89.1
|
||||
version: 1.90.0
|
||||
bigint-buffer:
|
||||
specifier: ^1.1.5
|
||||
version: 1.1.5
|
||||
@ -21,10 +24,10 @@ dependencies:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
dotenv:
|
||||
specifier: ^16.3.2
|
||||
specifier: ^16.4.1
|
||||
version: 16.4.1
|
||||
pino:
|
||||
specifier: ^8.17.2
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
pino-pretty:
|
||||
specifier: ^10.3.1
|
||||
@ -38,7 +41,7 @@ devDependencies:
|
||||
specifier: ^5.1.5
|
||||
version: 5.1.5
|
||||
prettier:
|
||||
specifier: ^3.2.1
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.5
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
@ -204,6 +207,24 @@ packages:
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/@solana/spl-token@0.4.0(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22):
|
||||
resolution: {integrity: sha512-jjBIBG9IsclqQVl5Y82npGE6utdCh7Z9VFcF5qgJa5EUq2XgspW3Dt1wujWjH/vQDRnkp9zGO+BqQU/HhX/3wg==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
'@solana/web3.js': ^1.89.1
|
||||
dependencies:
|
||||
'@solana/buffer-layout': 4.0.1
|
||||
'@solana/buffer-layout-utils': 0.2.0
|
||||
'@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.90.0)(fastestsmallesttextencoderdecoder@1.0.22)
|
||||
'@solana/web3.js': 1.90.0
|
||||
buffer: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- encoding
|
||||
- fastestsmallesttextencoderdecoder
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/@solana/spl-type-length-value@0.1.0:
|
||||
resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==}
|
||||
engines: {node: '>=16'}
|
||||
@ -260,7 +281,7 @@ packages:
|
||||
/@types/connect@3.4.38:
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
dependencies:
|
||||
'@types/node': 12.20.55
|
||||
'@types/node': 20.11.16
|
||||
dev: false
|
||||
|
||||
/@types/node@12.20.55:
|
||||
@ -271,12 +292,11 @@ packages:
|
||||
resolution: {integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==}
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
dev: true
|
||||
|
||||
/@types/ws@7.4.7:
|
||||
resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
|
||||
dependencies:
|
||||
'@types/node': 12.20.55
|
||||
'@types/node': 20.11.16
|
||||
dev: false
|
||||
|
||||
/JSONStream@1.3.5:
|
||||
@ -869,7 +889,6 @@ packages:
|
||||
|
||||
/undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
dev: true
|
||||
|
||||
/utf-8-validate@5.0.10:
|
||||
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
|
||||
|
||||
@ -3,10 +3,10 @@ import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
|
||||
const variable = process.env[variableName] || '';
|
||||
if (!variable) {
|
||||
logger.error(`${variableName} is not set`);
|
||||
process.exit(1);
|
||||
}
|
||||
return variable;
|
||||
const variable = process.env[variableName] || '';
|
||||
if (!variable) {
|
||||
logger.error(`${variableName} is not set`);
|
||||
process.exit(1);
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
|
||||
164
worker/watcher.ts
Normal file
164
worker/watcher.ts
Normal file
@ -0,0 +1,164 @@
|
||||
// sellWorker.ts
|
||||
import { parentPort, workerData } from 'worker_threads';
|
||||
import { PublicKey, TokenAmount, Connection, Commitment } from '@solana/web3.js';
|
||||
import { LIQUIDITY_STATE_LAYOUT_V4, Liquidity, SPL_ACCOUNT_LAYOUT, TOKEN_PROGRAM_ID, TokenAccount } from '@raydium-io/raydium-sdk';
|
||||
import { retrieveEnvVariable } from '../utils';
|
||||
import BN from 'bn.js';
|
||||
import pino from 'pino';
|
||||
|
||||
const transport = pino.transport({
|
||||
targets: [
|
||||
{
|
||||
level: 'trace',
|
||||
target: 'pino-pretty',
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const logger = pino(
|
||||
{
|
||||
redact: ['poolKeys'],
|
||||
serializers: {
|
||||
error: pino.stdSerializers.err,
|
||||
},
|
||||
base: undefined,
|
||||
},
|
||||
transport,
|
||||
);
|
||||
|
||||
async function getTokenAccounts(connection: Connection, owner: PublicKey) {
|
||||
const tokenResp = await connection.getTokenAccountsByOwner(owner, {
|
||||
programId: TOKEN_PROGRAM_ID
|
||||
});
|
||||
|
||||
const accounts: TokenAccount[] = [];
|
||||
for (const { pubkey, account } of tokenResp.value) {
|
||||
accounts.push({
|
||||
pubkey,
|
||||
accountInfo: SPL_ACCOUNT_LAYOUT.decode(account.data),
|
||||
programId: new PublicKey(account.owner.toBase58())
|
||||
});
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
const SOL_SDC_POOL_ID = "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2";
|
||||
const OPENBOOK_PROGRAM_ID = new PublicKey(
|
||||
"srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX"
|
||||
);
|
||||
|
||||
async function parsePoolInfo() {
|
||||
const network = 'mainnet-beta';
|
||||
const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
|
||||
const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable(
|
||||
'RPC_WEBSOCKET_ENDPOINT',
|
||||
logger,
|
||||
);
|
||||
|
||||
const connection = new Connection(RPC_ENDPOINT, {
|
||||
wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
|
||||
});
|
||||
const owner = new PublicKey("VnxDzsZ7chE88e9rB6UKztCt2HUwrkgCTx8WieWf5mM");
|
||||
|
||||
const tokenAccounts = await getTokenAccounts(connection, owner);
|
||||
|
||||
// example to get pool info
|
||||
const info = await connection.getAccountInfo(new PublicKey(SOL_SDC_POOL_ID));
|
||||
if (!info) {
|
||||
throw new Error("Pool not found");
|
||||
}
|
||||
|
||||
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(info.data);
|
||||
|
||||
const baseDecimal = 10 ** poolState.baseDecimal.toNumber();
|
||||
const quoteDecimal = 10 ** poolState.quoteDecimal.toNumber();
|
||||
|
||||
const baseTokenAmount = await connection.getTokenAccountBalance(
|
||||
poolState.baseVault
|
||||
)
|
||||
|
||||
const quoteTokenAmount = await connection.getTokenAccountBalance(
|
||||
poolState.quoteVault
|
||||
)
|
||||
|
||||
const basePnl = poolState.baseNeedTakePnl.toNumber() / baseDecimal;
|
||||
const quotePnl = poolState.quoteNeedTakePnl.toNumber() / quoteDecimal;
|
||||
|
||||
const base = (baseTokenAmount.value?.uiAmount || 0) - basePnl;
|
||||
const quote = (quoteTokenAmount.value?.uiAmount || 0) - quotePnl;
|
||||
|
||||
const denominator = new BN(10).pow(poolState.baseDecimal);
|
||||
|
||||
const addedLpAccount = tokenAccounts.find((a) => a.accountInfo.mint.equals(poolState.lpMint));
|
||||
|
||||
const message = `
|
||||
SOL - USDC Pool Info:
|
||||
|
||||
Pool total base: ${base},
|
||||
Pool total quote: ${quote},
|
||||
|
||||
Base vault balance: ${baseTokenAmount.value.uiAmount},
|
||||
Quote vault balance: ${quoteTokenAmount.value.uiAmount},
|
||||
|
||||
Base token decimals: ${poolState.baseDecimal.toNumber()},
|
||||
Quote token decimals: ${poolState.quoteDecimal.toNumber()},
|
||||
Total LP: ${poolState.lpReserve.div(denominator).toString()},
|
||||
|
||||
Added LP amount: ${(addedLpAccount?.accountInfo.amount.toNumber() || 0) / baseDecimal},
|
||||
`;
|
||||
|
||||
logger.info(message);
|
||||
|
||||
// send message to discord (embed)
|
||||
// post to discord webhook
|
||||
let embed = {
|
||||
embeds: [
|
||||
{
|
||||
title: "SOL - USDC Pool Info",
|
||||
description: `
|
||||
Pool total base: **${base}**,
|
||||
Pool total quote: **${quote}**,
|
||||
|
||||
Base vault balance: **${baseTokenAmount.value.uiAmount}**,
|
||||
Quote vault balance: **${quoteTokenAmount.value.uiAmount}**,
|
||||
|
||||
Base token decimals:** ${poolState.baseDecimal.toNumber()}**,
|
||||
Quote token decimals:** ${poolState.quoteDecimal.toNumber()}**,
|
||||
|
||||
Total LP: **${poolState.lpReserve.div(denominator).toString()}**,
|
||||
Added LP amount: **${(addedLpAccount?.accountInfo.amount.toNumber() || 0) / baseDecimal}**
|
||||
|
||||
Happy trading! 🚀
|
||||
`
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
const DISCORD_WEBHOOK = retrieveEnvVariable('DISCORD_WEBHOOK', logger);
|
||||
// use native fetch to post to discord
|
||||
fetch(DISCORD_WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(embed),
|
||||
});
|
||||
|
||||
logger.info("Message sent to Discord");
|
||||
|
||||
}
|
||||
|
||||
// Function to periodically check the pool
|
||||
async function checkPoolPeriodically(interval: number) {
|
||||
while (true) {
|
||||
await parsePoolInfo();
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
}
|
||||
|
||||
// Check pool periodically with a specified interval
|
||||
checkPoolPeriodically(60000); // 1 minute
|
||||
Reference in New Issue
Block a user