Add auto sell feature

This commit is contained in:
OneRobotBoii
2024-02-14 20:06:15 +07:00
parent 5bc7cec9bd
commit 71b02470cb
5 changed files with 251 additions and 27 deletions

60
buy.ts
View File

@ -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();

View File

@ -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
View File

@ -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==}

View File

@ -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
View 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