mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-09 20:12:06 +10:00
feat: complete rewrite
This commit is contained in:
44
.env.copy
44
.env.copy
@ -1,16 +1,40 @@
|
|||||||
|
# Wallet
|
||||||
PRIVATE_KEY=
|
PRIVATE_KEY=
|
||||||
|
|
||||||
|
# Connection
|
||||||
RPC_ENDPOINT=https://api.mainnet-beta.solana.com
|
RPC_ENDPOINT=https://api.mainnet-beta.solana.com
|
||||||
RPC_WEBSOCKET_ENDPOINT=wss://api.mainnet-beta.solana.com
|
RPC_WEBSOCKET_ENDPOINT=wss://api.mainnet-beta.solana.com
|
||||||
|
COMMITMENT_LEVEL=confirmed
|
||||||
|
|
||||||
|
# Bot
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
ONE_TOKEN_AT_A_TIME=true
|
||||||
|
COMPUTE_UNIT_LIMIT=421197
|
||||||
|
COMPUTE_UNIT_PRICE=101337
|
||||||
|
PRE_LOAD_EXISTING_MARKETS=false
|
||||||
|
CACHE_NEW_MARKETS=false
|
||||||
|
|
||||||
|
# Buy
|
||||||
QUOTE_MINT=WSOL
|
QUOTE_MINT=WSOL
|
||||||
QUOTE_AMOUNT=0.01
|
QUOTE_AMOUNT=0.001
|
||||||
COMMITMENT_LEVEL=finalized
|
AUTO_BUY_DELAY=0
|
||||||
|
MAX_BUY_RETRIES=10
|
||||||
|
BUY_SLIPPAGE=5
|
||||||
|
|
||||||
|
# Sell
|
||||||
|
AUTO_SELL=true
|
||||||
|
MAX_SELL_RETRIES=10
|
||||||
|
AUTO_SELL_DELAY=0
|
||||||
|
PRICE_CHECK_INTERVAL=2000
|
||||||
|
PRICE_CHECK_DURATION=60000
|
||||||
|
TAKE_PROFIT=25
|
||||||
|
STOP_LOSS=15
|
||||||
|
SELL_SLIPPAGE=5
|
||||||
|
|
||||||
|
# Filters
|
||||||
USE_SNIPE_LIST=false
|
USE_SNIPE_LIST=false
|
||||||
SNIPE_LIST_REFRESH_INTERVAL=30000
|
SNIPE_LIST_REFRESH_INTERVAL=30000
|
||||||
CHECK_IF_MINT_IS_RENOUNCED=false
|
CHECK_IF_MINT_IS_RENOUNCED=true
|
||||||
AUTO_SELL=true
|
CHECK_IF_BURNED=false
|
||||||
MAX_SELL_RETRIES=5
|
MIN_POOL_SIZE=5
|
||||||
AUTO_SELL_DELAY=1000
|
MAX_POOL_SIZE=50
|
||||||
LOG_LEVEL=info
|
|
||||||
MIN_POOL_SIZE=10
|
|
||||||
MAX_POOL_SIZE=50
|
|
||||||
ONE_TOKEN_AT_A_TIME=true
|
|
||||||
393
bot.ts
Normal file
393
bot.ts
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
import {
|
||||||
|
ComputeBudgetProgram,
|
||||||
|
Connection,
|
||||||
|
Keypair,
|
||||||
|
PublicKey,
|
||||||
|
TransactionMessage,
|
||||||
|
VersionedTransaction,
|
||||||
|
} from '@solana/web3.js';
|
||||||
|
import {
|
||||||
|
createAssociatedTokenAccountIdempotentInstruction,
|
||||||
|
createCloseAccountInstruction,
|
||||||
|
getAccount,
|
||||||
|
getAssociatedTokenAddress,
|
||||||
|
RawAccount,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
} from '@solana/spl-token';
|
||||||
|
import {
|
||||||
|
Liquidity,
|
||||||
|
LiquidityPoolKeysV4,
|
||||||
|
LiquidityStateV4,
|
||||||
|
Percent,
|
||||||
|
Token,
|
||||||
|
TokenAmount,
|
||||||
|
} from '@raydium-io/raydium-sdk';
|
||||||
|
import { MarketCache, PoolCache, SnipeListCache } from './cache';
|
||||||
|
import { PoolFilters } from './filters';
|
||||||
|
import { TransactionExecutor } from './transactions';
|
||||||
|
import { createPoolKeys, logger, NETWORK, sleep } from './helpers';
|
||||||
|
import { Mutex } from 'async-mutex';
|
||||||
|
import BN from 'bn.js';
|
||||||
|
|
||||||
|
export interface BotConfig {
|
||||||
|
wallet: Keypair;
|
||||||
|
checkRenounced: boolean;
|
||||||
|
checkBurned: boolean;
|
||||||
|
minPoolSize: TokenAmount;
|
||||||
|
maxPoolSize: TokenAmount;
|
||||||
|
quoteToken: Token;
|
||||||
|
quoteAmount: TokenAmount;
|
||||||
|
quoteAta: PublicKey;
|
||||||
|
oneTokenAtATime: boolean;
|
||||||
|
useSnipeList: boolean;
|
||||||
|
autoSell: boolean;
|
||||||
|
autoBuyDelay: number;
|
||||||
|
autoSellDelay: number;
|
||||||
|
maxBuyRetries: number;
|
||||||
|
maxSellRetries: number;
|
||||||
|
unitLimit: number;
|
||||||
|
unitPrice: number;
|
||||||
|
takeProfit: number;
|
||||||
|
stopLoss: number;
|
||||||
|
buySlippage: number;
|
||||||
|
sellSlippage: number;
|
||||||
|
priceCheckInterval: number;
|
||||||
|
priceCheckDuration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Bot {
|
||||||
|
private readonly poolFilters: PoolFilters;
|
||||||
|
|
||||||
|
// snipe list
|
||||||
|
private readonly snipeListCache?: SnipeListCache;
|
||||||
|
|
||||||
|
// one token at the time
|
||||||
|
private readonly mutex: Mutex;
|
||||||
|
private sellExecutionCount = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly connection: Connection,
|
||||||
|
private readonly marketStorage: MarketCache,
|
||||||
|
private readonly poolStorage: PoolCache,
|
||||||
|
private readonly txExecutor: TransactionExecutor,
|
||||||
|
private readonly config: BotConfig,
|
||||||
|
) {
|
||||||
|
this.mutex = new Mutex();
|
||||||
|
this.poolFilters = new PoolFilters(connection, {
|
||||||
|
quoteToken: this.config.quoteToken,
|
||||||
|
minPoolSize: this.config.minPoolSize,
|
||||||
|
maxPoolSize: this.config.maxPoolSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.config.useSnipeList) {
|
||||||
|
this.snipeListCache = new SnipeListCache();
|
||||||
|
this.snipeListCache.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate() {
|
||||||
|
try {
|
||||||
|
await getAccount(this.connection, this.config.quoteAta, this.connection.commitment);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`${this.config.quoteToken.symbol} token account not found in wallet: ${this.config.wallet.publicKey.toString()}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async buy(accountId: PublicKey, poolState: LiquidityStateV4) {
|
||||||
|
logger.trace({ mint: poolState.baseMint }, `Processing buy...`);
|
||||||
|
|
||||||
|
if (this.config.useSnipeList && !this.snipeListCache?.isInList(poolState.baseMint.toString())) {
|
||||||
|
logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because token is not in a snipe list`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.autoBuyDelay > 0) {
|
||||||
|
logger.debug({ mint: poolState.baseMint }, `Waiting for ${this.config.autoBuyDelay} ms before buy`);
|
||||||
|
await sleep(this.config.autoBuyDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.oneTokenAtATime) {
|
||||||
|
if (this.mutex.isLocked() || this.sellExecutionCount > 0) {
|
||||||
|
logger.debug(
|
||||||
|
{ mint: poolState.baseMint.toString() },
|
||||||
|
`Skipping buy because one token at a time is turned on and token is already being processed`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.mutex.acquire();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const shouldBuy = await this.poolFilters.execute(poolState);
|
||||||
|
|
||||||
|
if (!shouldBuy) {
|
||||||
|
logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because pool doesn't match filters`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.config.maxBuyRetries; i++) {
|
||||||
|
try {
|
||||||
|
const [market, mintAta] = await Promise.all([
|
||||||
|
this.marketStorage.get(poolState.marketId.toString()),
|
||||||
|
getAssociatedTokenAddress(poolState.baseMint, this.config.wallet.publicKey),
|
||||||
|
]);
|
||||||
|
const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{ mint: poolState.baseMint.toString() },
|
||||||
|
`Send buy transaction attempt: ${i + 1}/${this.config.maxBuyRetries}`,
|
||||||
|
);
|
||||||
|
const tokenOut = new Token(TOKEN_PROGRAM_ID, poolKeys.baseMint, poolKeys.baseDecimals);
|
||||||
|
const result = await this.swap(
|
||||||
|
poolKeys,
|
||||||
|
this.config.quoteAta,
|
||||||
|
mintAta,
|
||||||
|
this.config.quoteToken,
|
||||||
|
tokenOut,
|
||||||
|
this.config.quoteAmount,
|
||||||
|
this.config.buySlippage,
|
||||||
|
this.config.wallet,
|
||||||
|
'buy',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.confirmed) {
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
mint: poolState.baseMint.toString(),
|
||||||
|
signature: result.signature,
|
||||||
|
url: `https://solscan.io/tx/${result.signature}?cluster=${NETWORK}`,
|
||||||
|
},
|
||||||
|
`Confirmed buy tx`,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
{
|
||||||
|
mint: poolState.baseMint.toString(),
|
||||||
|
signature: result.signature,
|
||||||
|
},
|
||||||
|
`Error confirming buy tx`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug({ mint: poolState.baseMint.toString(), error }, `Error confirming buy transaction`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ mint: poolState.baseMint.toString(), error }, `Failed to buy token`);
|
||||||
|
} finally {
|
||||||
|
if (this.config.oneTokenAtATime) {
|
||||||
|
this.mutex.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sell(accountId: PublicKey, rawAccount: RawAccount) {
|
||||||
|
if (this.config.oneTokenAtATime) {
|
||||||
|
this.sellExecutionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.trace({ mint: rawAccount.mint }, `Processing sell...`);
|
||||||
|
|
||||||
|
const poolData = await this.poolStorage.get(rawAccount.mint.toString());
|
||||||
|
|
||||||
|
if (!poolData) {
|
||||||
|
logger.trace({ mint: rawAccount.mint.toString() }, `Token pool data is not found, can't sell`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenIn = new Token(TOKEN_PROGRAM_ID, poolData.state.baseMint, poolData.state.baseDecimal.toNumber());
|
||||||
|
const tokenAmountIn = new TokenAmount(tokenIn, rawAccount.amount, true);
|
||||||
|
|
||||||
|
if (tokenAmountIn.isZero()) {
|
||||||
|
logger.info({ mint: rawAccount.mint.toString() }, `Empty balance, can't sell`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.autoSellDelay > 0) {
|
||||||
|
logger.debug({ mint: rawAccount.mint }, `Waiting for ${this.config.autoSellDelay} ms before sell`);
|
||||||
|
await sleep(this.config.autoSellDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.config.maxSellRetries; i++) {
|
||||||
|
try {
|
||||||
|
const market = await this.marketStorage.get(poolData.state.marketId.toString());
|
||||||
|
const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(new PublicKey(poolData.id), poolData.state, market);
|
||||||
|
|
||||||
|
await this.priceMatch(tokenAmountIn, poolKeys);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{ mint: rawAccount.mint },
|
||||||
|
`Send sell transaction attempt: ${i + 1}/${this.config.maxSellRetries}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await this.swap(
|
||||||
|
poolKeys,
|
||||||
|
accountId,
|
||||||
|
this.config.quoteAta,
|
||||||
|
tokenIn,
|
||||||
|
this.config.quoteToken,
|
||||||
|
tokenAmountIn,
|
||||||
|
this.config.sellSlippage,
|
||||||
|
this.config.wallet,
|
||||||
|
'sell',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.confirmed) {
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
dex: `https://dexscreener.com/solana/${rawAccount.mint.toString()}?maker=${this.config.wallet.publicKey}`,
|
||||||
|
mint: rawAccount.mint.toString(),
|
||||||
|
signature: result.signature,
|
||||||
|
url: `https://solscan.io/tx/${result.signature}?cluster=${NETWORK}`,
|
||||||
|
},
|
||||||
|
`Confirmed sell tx`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
mint: rawAccount.mint.toString(),
|
||||||
|
signature: result.signature,
|
||||||
|
},
|
||||||
|
`Error confirming sell tx`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug({ mint: rawAccount.mint.toString(), error }, `Error confirming sell transaction`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`);
|
||||||
|
} finally {
|
||||||
|
if (this.config.oneTokenAtATime) {
|
||||||
|
this.sellExecutionCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async swap(
|
||||||
|
poolKeys: LiquidityPoolKeysV4,
|
||||||
|
ataIn: PublicKey,
|
||||||
|
ataOut: PublicKey,
|
||||||
|
tokenIn: Token,
|
||||||
|
tokenOut: Token,
|
||||||
|
amountIn: TokenAmount,
|
||||||
|
slippage: number,
|
||||||
|
wallet: Keypair,
|
||||||
|
direction: 'buy' | 'sell',
|
||||||
|
) {
|
||||||
|
const slippagePercent = new Percent(slippage, 100);
|
||||||
|
const poolInfo = await Liquidity.fetchInfo({
|
||||||
|
connection: this.connection,
|
||||||
|
poolKeys,
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedAmountOut = Liquidity.computeAmountOut({
|
||||||
|
poolKeys,
|
||||||
|
poolInfo,
|
||||||
|
amountIn,
|
||||||
|
currencyOut: tokenOut,
|
||||||
|
slippage: slippagePercent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestBlockhash = await this.connection.getLatestBlockhash();
|
||||||
|
const { innerTransaction } = Liquidity.makeSwapFixedInInstruction(
|
||||||
|
{
|
||||||
|
poolKeys: poolKeys,
|
||||||
|
userKeys: {
|
||||||
|
tokenAccountIn: ataIn,
|
||||||
|
tokenAccountOut: ataOut,
|
||||||
|
owner: wallet.publicKey,
|
||||||
|
},
|
||||||
|
amountIn: amountIn.raw,
|
||||||
|
minAmountOut: computedAmountOut.minAmountOut.raw,
|
||||||
|
},
|
||||||
|
poolKeys.version,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageV0 = new TransactionMessage({
|
||||||
|
payerKey: wallet.publicKey,
|
||||||
|
recentBlockhash: latestBlockhash.blockhash,
|
||||||
|
instructions: [
|
||||||
|
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: this.config.unitPrice }),
|
||||||
|
ComputeBudgetProgram.setComputeUnitLimit({ units: this.config.unitLimit }),
|
||||||
|
...(direction === 'buy'
|
||||||
|
? [
|
||||||
|
createAssociatedTokenAccountIdempotentInstruction(
|
||||||
|
wallet.publicKey,
|
||||||
|
ataOut,
|
||||||
|
wallet.publicKey,
|
||||||
|
tokenOut.mint,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...innerTransaction.instructions,
|
||||||
|
...(direction === 'sell' ? [createCloseAccountInstruction(ataIn, wallet.publicKey, wallet.publicKey)] : []),
|
||||||
|
],
|
||||||
|
}).compileToV0Message();
|
||||||
|
|
||||||
|
const transaction = new VersionedTransaction(messageV0);
|
||||||
|
transaction.sign([wallet, ...innerTransaction.signers]);
|
||||||
|
|
||||||
|
return this.txExecutor.executeAndConfirm(transaction, latestBlockhash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
|
||||||
|
const profitFraction = this.config.quoteAmount.mul(this.config.takeProfit).numerator.div(new BN(100));
|
||||||
|
const profitAmount = new TokenAmount(this.config.quoteToken, profitFraction, true);
|
||||||
|
const takeProfit = this.config.quoteAmount.add(profitAmount);
|
||||||
|
|
||||||
|
const lossFraction = this.config.quoteAmount.mul(this.config.stopLoss).numerator.div(new BN(100));
|
||||||
|
const lossAmount = new TokenAmount(this.config.quoteToken, lossFraction, true);
|
||||||
|
const stopLoss = this.config.quoteAmount.subtract(lossAmount);
|
||||||
|
const slippage = new Percent(this.config.sellSlippage, 100);
|
||||||
|
|
||||||
|
const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
|
||||||
|
let timesChecked = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
const poolInfo = await Liquidity.fetchInfo({
|
||||||
|
connection: this.connection,
|
||||||
|
poolKeys,
|
||||||
|
});
|
||||||
|
|
||||||
|
const amountOut = Liquidity.computeAmountOut({
|
||||||
|
poolKeys,
|
||||||
|
poolInfo,
|
||||||
|
amountIn: amountIn,
|
||||||
|
currencyOut: this.config.quoteToken,
|
||||||
|
slippage,
|
||||||
|
}).amountOut;
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
{ mint: poolKeys.baseMint.toString() },
|
||||||
|
`Take profit: ${takeProfit.toFixed()} | Stop loss: ${stopLoss.toFixed()} | Current: ${amountOut.toFixed()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (amountOut.lt(stopLoss)){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountOut.gt(takeProfit)){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(this.config.priceCheckInterval);
|
||||||
|
} catch (e) {
|
||||||
|
logger.trace({ mint: poolKeys.baseMint.toString(), e }, `Failed to check token price`);
|
||||||
|
} finally {
|
||||||
|
timesChecked++;
|
||||||
|
}
|
||||||
|
} while (timesChecked < timesToCheck);
|
||||||
|
}
|
||||||
|
}
|
||||||
555
buy.ts
555
buy.ts
@ -1,555 +0,0 @@
|
|||||||
import {
|
|
||||||
BigNumberish,
|
|
||||||
Liquidity,
|
|
||||||
LIQUIDITY_STATE_LAYOUT_V4,
|
|
||||||
LiquidityPoolKeys,
|
|
||||||
LiquidityStateV4,
|
|
||||||
MARKET_STATE_LAYOUT_V3,
|
|
||||||
MarketStateV3,
|
|
||||||
Token,
|
|
||||||
TokenAmount,
|
|
||||||
} from '@raydium-io/raydium-sdk';
|
|
||||||
import {
|
|
||||||
AccountLayout,
|
|
||||||
createAssociatedTokenAccountIdempotentInstruction,
|
|
||||||
createCloseAccountInstruction,
|
|
||||||
getAssociatedTokenAddressSync,
|
|
||||||
TOKEN_PROGRAM_ID,
|
|
||||||
} from '@solana/spl-token';
|
|
||||||
import {
|
|
||||||
Keypair,
|
|
||||||
Connection,
|
|
||||||
PublicKey,
|
|
||||||
ComputeBudgetProgram,
|
|
||||||
KeyedAccountInfo,
|
|
||||||
TransactionMessage,
|
|
||||||
VersionedTransaction,
|
|
||||||
} from '@solana/web3.js';
|
|
||||||
import { getTokenAccounts, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, OPENBOOK_PROGRAM_ID, createPoolKeys } from './liquidity';
|
|
||||||
import { logger } from './utils';
|
|
||||||
import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market';
|
|
||||||
import { MintLayout } from './types';
|
|
||||||
import bs58 from 'bs58';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import {
|
|
||||||
AUTO_SELL,
|
|
||||||
AUTO_SELL_DELAY,
|
|
||||||
CHECK_IF_MINT_IS_RENOUNCED,
|
|
||||||
COMMITMENT_LEVEL,
|
|
||||||
LOG_LEVEL,
|
|
||||||
MAX_SELL_RETRIES,
|
|
||||||
NETWORK,
|
|
||||||
PRIVATE_KEY,
|
|
||||||
QUOTE_AMOUNT,
|
|
||||||
QUOTE_MINT,
|
|
||||||
RPC_ENDPOINT,
|
|
||||||
RPC_WEBSOCKET_ENDPOINT,
|
|
||||||
SNIPE_LIST_REFRESH_INTERVAL,
|
|
||||||
USE_SNIPE_LIST,
|
|
||||||
MIN_POOL_SIZE,
|
|
||||||
MAX_POOL_SIZE,
|
|
||||||
ONE_TOKEN_AT_A_TIME,
|
|
||||||
} from './constants';
|
|
||||||
|
|
||||||
const solanaConnection = new Connection(RPC_ENDPOINT, {
|
|
||||||
wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface MinimalTokenAccountData {
|
|
||||||
mint: PublicKey;
|
|
||||||
address: PublicKey;
|
|
||||||
poolKeys?: LiquidityPoolKeys;
|
|
||||||
market?: MinimalMarketLayoutV3;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingLiquidityPools: Set<string> = new Set<string>();
|
|
||||||
const existingOpenBookMarkets: Set<string> = new Set<string>();
|
|
||||||
const existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<string, MinimalTokenAccountData>();
|
|
||||||
|
|
||||||
let wallet: Keypair;
|
|
||||||
let quoteToken: Token;
|
|
||||||
let quoteTokenAssociatedAddress: PublicKey;
|
|
||||||
let quoteAmount: TokenAmount;
|
|
||||||
let quoteMinPoolSizeAmount: TokenAmount;
|
|
||||||
let quoteMaxPoolSizeAmount: TokenAmount;
|
|
||||||
let processingToken: Boolean = false;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let snipeList: string[] = [];
|
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
|
||||||
logger.level = LOG_LEVEL;
|
|
||||||
|
|
||||||
// get wallet
|
|
||||||
wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));
|
|
||||||
logger.info(`Wallet Address: ${wallet.publicKey}`);
|
|
||||||
|
|
||||||
// get quote mint and amount
|
|
||||||
switch (QUOTE_MINT) {
|
|
||||||
case 'WSOL': {
|
|
||||||
quoteToken = Token.WSOL;
|
|
||||||
quoteAmount = new TokenAmount(Token.WSOL, QUOTE_AMOUNT, false);
|
|
||||||
quoteMinPoolSizeAmount = new TokenAmount(quoteToken, MIN_POOL_SIZE, false);
|
|
||||||
quoteMaxPoolSizeAmount = new TokenAmount(quoteToken, MAX_POOL_SIZE, false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'USDC': {
|
|
||||||
quoteToken = new Token(
|
|
||||||
TOKEN_PROGRAM_ID,
|
|
||||||
new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
|
|
||||||
6,
|
|
||||||
'USDC',
|
|
||||||
'USDC',
|
|
||||||
);
|
|
||||||
quoteAmount = new TokenAmount(quoteToken, QUOTE_AMOUNT, false);
|
|
||||||
quoteMinPoolSizeAmount = new TokenAmount(quoteToken, MIN_POOL_SIZE, false);
|
|
||||||
quoteMaxPoolSizeAmount = new TokenAmount(quoteToken, MAX_POOL_SIZE, false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Snipe list: ${USE_SNIPE_LIST}`);
|
|
||||||
logger.info(`Check mint renounced: ${CHECK_IF_MINT_IS_RENOUNCED}`);
|
|
||||||
logger.info(
|
|
||||||
`Min pool size: ${quoteMinPoolSizeAmount.isZero() ? 'false' : quoteMinPoolSizeAmount.toFixed()} ${quoteToken.symbol}`,
|
|
||||||
);
|
|
||||||
logger.info(
|
|
||||||
`Max pool size: ${quoteMaxPoolSizeAmount.isZero() ? 'false' : quoteMaxPoolSizeAmount.toFixed()} ${quoteToken.symbol}`,
|
|
||||||
);
|
|
||||||
logger.info(`One token at a time: ${ONE_TOKEN_AT_A_TIME}`);
|
|
||||||
logger.info(`Buy amount: ${quoteAmount.toFixed()} ${quoteToken.symbol}`);
|
|
||||||
logger.info(`Auto sell: ${AUTO_SELL}`);
|
|
||||||
logger.info(`Sell delay: ${AUTO_SELL_DELAY === 0 ? 'false' : AUTO_SELL_DELAY}`);
|
|
||||||
|
|
||||||
// check existing wallet for associated token account of quote mint
|
|
||||||
const tokenAccounts = await getTokenAccounts(solanaConnection, wallet.publicKey, COMMITMENT_LEVEL);
|
|
||||||
|
|
||||||
for (const ta of tokenAccounts) {
|
|
||||||
existingTokenAccounts.set(ta.accountInfo.mint.toString(), <MinimalTokenAccountData>{
|
|
||||||
mint: ta.accountInfo.mint,
|
|
||||||
address: ta.pubkey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenAccount = tokenAccounts.find((acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString())!;
|
|
||||||
|
|
||||||
if (!tokenAccount) {
|
|
||||||
throw new Error(`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
quoteTokenAssociatedAddress = tokenAccount.pubkey;
|
|
||||||
|
|
||||||
// load tokens to snipe
|
|
||||||
loadSnipeList();
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) {
|
|
||||||
const ata = getAssociatedTokenAddressSync(mint, wallet.publicKey);
|
|
||||||
const tokenAccount = <MinimalTokenAccountData>{
|
|
||||||
address: ata,
|
|
||||||
mint: mint,
|
|
||||||
market: <MinimalMarketLayoutV3>{
|
|
||||||
bids: accountData.bids,
|
|
||||||
asks: accountData.asks,
|
|
||||||
eventQueue: accountData.eventQueue,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
existingTokenAccounts.set(mint.toString(), tokenAccount);
|
|
||||||
return tokenAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) {
|
|
||||||
if (!shouldBuy(poolState.baseMint.toString())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quoteMinPoolSizeAmount.isZero()) {
|
|
||||||
const poolSize = new TokenAmount(quoteToken, poolState.swapQuoteInAmount, true);
|
|
||||||
logger.info(`Processing pool: ${id.toString()} with ${poolSize.toFixed()} ${quoteToken.symbol} in liquidity`);
|
|
||||||
|
|
||||||
if (poolSize.lt(quoteMinPoolSizeAmount)) {
|
|
||||||
logger.warn(
|
|
||||||
{
|
|
||||||
mint: poolState.baseMint,
|
|
||||||
pooled: `${poolSize.toFixed()} ${quoteToken.symbol}`,
|
|
||||||
},
|
|
||||||
`Skipping pool, smaller than ${quoteMinPoolSizeAmount.toFixed()} ${quoteToken.symbol}`,
|
|
||||||
`Swap quote in amount: ${poolSize.toFixed()}`,
|
|
||||||
);
|
|
||||||
logger.info(`-------------------🤖🔧------------------- \n`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quoteMaxPoolSizeAmount.isZero()) {
|
|
||||||
const poolSize = new TokenAmount(quoteToken, poolState.swapQuoteInAmount, true);
|
|
||||||
|
|
||||||
if (poolSize.gt(quoteMaxPoolSizeAmount)) {
|
|
||||||
logger.warn(
|
|
||||||
{
|
|
||||||
mint: poolState.baseMint,
|
|
||||||
pooled: `${poolSize.toFixed()} ${quoteToken.symbol}`,
|
|
||||||
},
|
|
||||||
`Skipping pool, bigger than ${quoteMaxPoolSizeAmount.toFixed()} ${quoteToken.symbol}`,
|
|
||||||
`Swap quote in amount: ${poolSize.toFixed()}`,
|
|
||||||
);
|
|
||||||
logger.info(`-------------------🤖🔧------------------- \n`);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await buy(id, poolState);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkMintable(vault: PublicKey): Promise<boolean | undefined> {
|
|
||||||
try {
|
|
||||||
let { data } = (await solanaConnection.getAccountInfo(vault)) || {};
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const deserialize = MintLayout.decode(data);
|
|
||||||
return deserialize.mintAuthorityOption === 0;
|
|
||||||
} catch (e) {
|
|
||||||
logger.debug(e);
|
|
||||||
logger.error({ mint: vault }, `Failed to check if mint is renounced`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) {
|
|
||||||
let accountData: MarketStateV3 | undefined;
|
|
||||||
try {
|
|
||||||
accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
|
|
||||||
|
|
||||||
// to be competitive, we collect market data before buying the token...
|
|
||||||
if (existingTokenAccounts.has(accountData.baseMint.toString())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTokenAccount(accountData.baseMint, accountData);
|
|
||||||
} catch (e) {
|
|
||||||
logger.debug(e);
|
|
||||||
logger.error({ mint: accountData?.baseMint }, `Failed to process market`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise<void> {
|
|
||||||
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_LEVEL);
|
|
||||||
tokenAccount = saveTokenAccount(accountData.baseMint, market);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
tokenAccount.poolKeys.version,
|
|
||||||
);
|
|
||||||
|
|
||||||
const latestBlockhash = await solanaConnection.getLatestBlockhash({
|
|
||||||
commitment: COMMITMENT_LEVEL,
|
|
||||||
});
|
|
||||||
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_LEVEL,
|
|
||||||
});
|
|
||||||
logger.info({ mint: accountData.baseMint, signature }, `Sent buy tx`);
|
|
||||||
processingToken = true;
|
|
||||||
|
|
||||||
const confirmation = await solanaConnection.confirmTransaction(
|
|
||||||
{
|
|
||||||
signature,
|
|
||||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
|
||||||
blockhash: latestBlockhash.blockhash,
|
|
||||||
},
|
|
||||||
COMMITMENT_LEVEL,
|
|
||||||
);
|
|
||||||
if (!confirmation.value.err) {
|
|
||||||
logger.info(`-------------------🟢------------------- `);
|
|
||||||
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);
|
|
||||||
processingToken = false;
|
|
||||||
logger.error({ mint: accountData.baseMint }, `Failed to buy token`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sell(accountId: PublicKey, mint: PublicKey, amount: BigNumberish): Promise<void> {
|
|
||||||
let sold = false;
|
|
||||||
let retries = 0;
|
|
||||||
|
|
||||||
if (AUTO_SELL_DELAY > 0) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, AUTO_SELL_DELAY));
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try {
|
|
||||||
const tokenAccount = existingTokenAccounts.get(mint.toString());
|
|
||||||
|
|
||||||
if (!tokenAccount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenAccount.poolKeys) {
|
|
||||||
logger.warn({ mint }, 'No pool keys found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amount === 0) {
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
mint: tokenAccount.mint,
|
|
||||||
},
|
|
||||||
`Empty balance, can't sell`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_LEVEL,
|
|
||||||
});
|
|
||||||
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_LEVEL,
|
|
||||||
});
|
|
||||||
logger.info({ mint, signature }, `Sent sell tx`);
|
|
||||||
const confirmation = await solanaConnection.confirmTransaction(
|
|
||||||
{
|
|
||||||
signature,
|
|
||||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
|
||||||
blockhash: latestBlockhash.blockhash,
|
|
||||||
},
|
|
||||||
COMMITMENT_LEVEL,
|
|
||||||
);
|
|
||||||
if (confirmation.value.err) {
|
|
||||||
logger.debug(confirmation.value.err);
|
|
||||||
logger.info({ mint, signature }, `Error confirming sell tx`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
logger.info(`-------------------🔴------------------- `);
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
dex: `https://dexscreener.com/solana/${mint}?maker=${wallet.publicKey}`,
|
|
||||||
mint,
|
|
||||||
signature,
|
|
||||||
url: `https://solscan.io/tx/${signature}?cluster=${NETWORK}`,
|
|
||||||
},
|
|
||||||
`Confirmed sell tx`,
|
|
||||||
);
|
|
||||||
sold = true;
|
|
||||||
processingToken = false;
|
|
||||||
} catch (e: any) {
|
|
||||||
// wait for a bit before retrying
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
retries++;
|
|
||||||
logger.debug(e);
|
|
||||||
logger.error({ mint }, `Failed to sell token, retry: ${retries}/${MAX_SELL_RETRIES}`);
|
|
||||||
}
|
|
||||||
} while (!sold && retries < MAX_SELL_RETRIES);
|
|
||||||
processingToken = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSnipeList() {
|
|
||||||
if (!USE_SNIPE_LIST) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = snipeList.length;
|
|
||||||
const data = fs.readFileSync(path.join(__dirname, 'snipe-list.txt'), 'utf-8');
|
|
||||||
snipeList = data
|
|
||||||
.split('\n')
|
|
||||||
.map((a) => a.trim())
|
|
||||||
.filter((a) => a);
|
|
||||||
|
|
||||||
if (snipeList.length != count) {
|
|
||||||
logger.info(`Loaded snipe list: ${snipeList.length}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldBuy(key: string): boolean {
|
|
||||||
logger.info(`-------------------🤖🔧------------------- `);
|
|
||||||
logger.info(`Processing token: ${processingToken}`)
|
|
||||||
return USE_SNIPE_LIST ? snipeList.includes(key) : ONE_TOKEN_AT_A_TIME ? !processingToken : true
|
|
||||||
}
|
|
||||||
|
|
||||||
const runListener = async () => {
|
|
||||||
await init();
|
|
||||||
const runTimestamp = Math.floor(new Date().getTime() / 1000);
|
|
||||||
const raydiumSubscriptionId = solanaConnection.onProgramAccountChange(
|
|
||||||
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
|
||||||
async (updatedAccountInfo) => {
|
|
||||||
const key = updatedAccountInfo.accountId.toString();
|
|
||||||
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
|
|
||||||
const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
|
|
||||||
const existing = existingLiquidityPools.has(key);
|
|
||||||
|
|
||||||
if (poolOpenTime > runTimestamp && !existing) {
|
|
||||||
existingLiquidityPools.add(key);
|
|
||||||
const _ = processRaydiumPool(updatedAccountInfo.accountId, poolState);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
COMMITMENT_LEVEL,
|
|
||||||
[
|
|
||||||
{ dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
|
|
||||||
{
|
|
||||||
memcmp: {
|
|
||||||
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
|
|
||||||
bytes: quoteToken.mint.toBase58(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memcmp: {
|
|
||||||
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'),
|
|
||||||
bytes: OPENBOOK_PROGRAM_ID.toBase58(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memcmp: {
|
|
||||||
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
|
|
||||||
bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const openBookSubscriptionId = solanaConnection.onProgramAccountChange(
|
|
||||||
OPENBOOK_PROGRAM_ID,
|
|
||||||
async (updatedAccountInfo) => {
|
|
||||||
const key = updatedAccountInfo.accountId.toString();
|
|
||||||
const existing = existingOpenBookMarkets.has(key);
|
|
||||||
if (!existing) {
|
|
||||||
existingOpenBookMarkets.add(key);
|
|
||||||
const _ = processOpenBookMarket(updatedAccountInfo);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
COMMITMENT_LEVEL,
|
|
||||||
[
|
|
||||||
{ dataSize: MARKET_STATE_LAYOUT_V3.span },
|
|
||||||
{
|
|
||||||
memcmp: {
|
|
||||||
offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
|
|
||||||
bytes: quoteToken.mint.toBase58(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
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_LEVEL,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
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}`);
|
|
||||||
|
|
||||||
logger.info('------------------- 🚀 ---------------------');
|
|
||||||
logger.info('Bot is running! Press CTRL + C to stop it.');
|
|
||||||
logger.info('------------------- 🚀 ---------------------');
|
|
||||||
|
|
||||||
if (USE_SNIPE_LIST) {
|
|
||||||
setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
runListener();
|
|
||||||
3
cache/index.ts
vendored
Normal file
3
cache/index.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './market.cache';
|
||||||
|
export * from './pool.cache';
|
||||||
|
export * from './snipe-list.cache';
|
||||||
58
cache/market.cache.ts
vendored
Normal file
58
cache/market.cache.ts
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Connection, PublicKey } from '@solana/web3.js';
|
||||||
|
import { getMinimalMarketV3, logger, MINIMAL_MARKET_STATE_LAYOUT_V3, MinimalMarketLayoutV3 } from '../helpers';
|
||||||
|
import { MAINNET_PROGRAM_ID, MARKET_STATE_LAYOUT_V3, Token } from '@raydium-io/raydium-sdk';
|
||||||
|
|
||||||
|
export class MarketCache {
|
||||||
|
private readonly keys: Map<string, MinimalMarketLayoutV3> = new Map<string, MinimalMarketLayoutV3>();
|
||||||
|
constructor(private readonly connection: Connection) {}
|
||||||
|
|
||||||
|
async init(config: { quoteToken: Token }) {
|
||||||
|
logger.debug({}, `Fetching all existing ${config.quoteToken.symbol} markets...`);
|
||||||
|
|
||||||
|
const accounts = await this.connection.getProgramAccounts(MAINNET_PROGRAM_ID.OPENBOOK_MARKET, {
|
||||||
|
commitment: this.connection.commitment,
|
||||||
|
dataSlice: {
|
||||||
|
offset: MARKET_STATE_LAYOUT_V3.offsetOf('eventQueue'),
|
||||||
|
length: MINIMAL_MARKET_STATE_LAYOUT_V3.span,
|
||||||
|
},
|
||||||
|
filters: [
|
||||||
|
{ dataSize: MARKET_STATE_LAYOUT_V3.span },
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
|
||||||
|
bytes: config.quoteToken.mint.toBase58(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
const market = MINIMAL_MARKET_STATE_LAYOUT_V3.decode(account.account.data);
|
||||||
|
this.keys.set(account.pubkey.toString(), market);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug({}, `Cached ${this.keys.size} markets`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public save(marketId: string, keys: MinimalMarketLayoutV3) {
|
||||||
|
if (!this.keys.has(marketId)) {
|
||||||
|
logger.trace({}, `Caching new market: ${marketId}`);
|
||||||
|
this.keys.set(marketId, keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(marketId: string): Promise<MinimalMarketLayoutV3> {
|
||||||
|
if (this.keys.has(marketId)) {
|
||||||
|
return this.keys.get(marketId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace({}, `Fetching new market keys for ${marketId}`);
|
||||||
|
const market = await this.fetch(marketId);
|
||||||
|
this.keys.set(marketId, market);
|
||||||
|
return market;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetch(marketId: string): Promise<MinimalMarketLayoutV3> {
|
||||||
|
return getMinimalMarketV3(this.connection, new PublicKey(marketId), this.connection.commitment);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
cache/pool.cache.ts
vendored
Normal file
20
cache/pool.cache.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
|
||||||
|
import { logger } from '../helpers';
|
||||||
|
|
||||||
|
export class PoolCache {
|
||||||
|
private readonly keys: Map<string, { id: string; state: LiquidityStateV4 }> = new Map<
|
||||||
|
string,
|
||||||
|
{ id: string; state: LiquidityStateV4 }
|
||||||
|
>();
|
||||||
|
|
||||||
|
public save(id: string, state: LiquidityStateV4) {
|
||||||
|
if (!this.keys.has(state.baseMint.toString())) {
|
||||||
|
logger.trace(`Caching new pool for mint: ${state.baseMint.toString()}`);
|
||||||
|
this.keys.set(state.baseMint.toString(), { id, state });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(mint: string): Promise<{ id: string; state: LiquidityStateV4 }> {
|
||||||
|
return this.keys.get(mint)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
cache/snipe-list.cache.ts
vendored
Normal file
34
cache/snipe-list.cache.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { logger, SNIPE_LIST_REFRESH_INTERVAL } from '../helpers';
|
||||||
|
|
||||||
|
export class SnipeListCache {
|
||||||
|
private snipeList: string[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
setInterval(this.loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
this.loadSnipeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInList(mint: string) {
|
||||||
|
return this.snipeList.includes(mint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadSnipeList() {
|
||||||
|
logger.trace('Refreshing snipe list...');
|
||||||
|
|
||||||
|
const count = this.snipeList.length;
|
||||||
|
const data = fs.readFileSync(path.join(__dirname, 'snipe-list.txt'), 'utf-8');
|
||||||
|
this.snipeList = data
|
||||||
|
.split('\n')
|
||||||
|
.map((a) => a.trim())
|
||||||
|
.filter((a) => a);
|
||||||
|
|
||||||
|
if (this.snipeList.length != count) {
|
||||||
|
logger.info(`Loaded snipe list: ${this.snipeList.length}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { Commitment } from "@solana/web3.js";
|
|
||||||
import { logger, retrieveEnvVariable } from "../utils";
|
|
||||||
|
|
||||||
export const NETWORK = 'mainnet-beta';
|
|
||||||
export const COMMITMENT_LEVEL: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
|
|
||||||
export const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
|
|
||||||
export const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
|
|
||||||
export const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL', logger);
|
|
||||||
export const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
|
|
||||||
export const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
|
|
||||||
export const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
|
|
||||||
export const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
|
|
||||||
export const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
|
|
||||||
export const AUTO_SELL_DELAY = Number(retrieveEnvVariable('AUTO_SELL_DELAY', logger));
|
|
||||||
export const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
|
|
||||||
export const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger);
|
|
||||||
export const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger);
|
|
||||||
export const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger);
|
|
||||||
export const MAX_POOL_SIZE = retrieveEnvVariable('MAX_POOL_SIZE', logger);
|
|
||||||
export const ONE_TOKEN_AT_A_TIME = retrieveEnvVariable('ONE_TOKEN_AT_A_TIME', logger) === 'true';
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './constants';
|
|
||||||
24
filters/burn.filter.ts
Normal file
24
filters/burn.filter.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Filter, FilterResult } from './pool-filters';
|
||||||
|
import { Connection } from '@solana/web3.js';
|
||||||
|
import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
|
||||||
|
import { logger } from '../helpers';
|
||||||
|
|
||||||
|
export class BurnFilter implements Filter {
|
||||||
|
constructor(private readonly connection: Connection) {}
|
||||||
|
|
||||||
|
async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
|
||||||
|
try {
|
||||||
|
const amount = await this.connection.getTokenSupply(poolState.lpMint, this.connection.commitment);
|
||||||
|
const burned = amount.value.uiAmount === 0;
|
||||||
|
return { ok: burned, message: burned ? undefined : "Burned -> Creator didn't burn LP" };
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.code == -32602) {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error({ mint: poolState.baseMint }, `Failed to check if LP is burned`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, message: 'Failed to check if LP is burned' };
|
||||||
|
}
|
||||||
|
}
|
||||||
4
filters/index.ts
Normal file
4
filters/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './burn.filter';
|
||||||
|
export * from './pool-filters';
|
||||||
|
export * from './pool-size.filter';
|
||||||
|
export * from './renounced.filter';
|
||||||
61
filters/pool-filters.ts
Normal file
61
filters/pool-filters.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { Connection } from '@solana/web3.js';
|
||||||
|
import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
|
||||||
|
import { BurnFilter } from './burn.filter';
|
||||||
|
import { RenouncedFilter } from './renounced.filter';
|
||||||
|
import { PoolSizeFilter } from './pool-size.filter';
|
||||||
|
import { CHECK_IF_BURNED, CHECK_IF_MINT_IS_RENOUNCED, logger } from '../helpers';
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
execute(poolState: LiquidityStateV4): Promise<FilterResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterResult {
|
||||||
|
ok: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PoolFilterArgs {
|
||||||
|
minPoolSize: TokenAmount;
|
||||||
|
maxPoolSize: TokenAmount;
|
||||||
|
quoteToken: Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PoolFilters {
|
||||||
|
private readonly filters: Filter[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly connection: Connection,
|
||||||
|
readonly args: PoolFilterArgs,
|
||||||
|
) {
|
||||||
|
if (CHECK_IF_BURNED) {
|
||||||
|
this.filters.push(new BurnFilter(connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CHECK_IF_MINT_IS_RENOUNCED) {
|
||||||
|
this.filters.push(new RenouncedFilter(connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.minPoolSize.isZero() || !args.maxPoolSize.isZero()) {
|
||||||
|
this.filters.push(new PoolSizeFilter(connection, args.quoteToken, args.minPoolSize, args.maxPoolSize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async execute(poolState: LiquidityStateV4): Promise<boolean> {
|
||||||
|
if (this.filters.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Promise.all(this.filters.map((f) => f.execute(poolState)));
|
||||||
|
const pass = result.every((r) => r.ok);
|
||||||
|
|
||||||
|
if (pass) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const filterResult of result.filter((r) => !r.ok)) {
|
||||||
|
logger.info(filterResult.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
filters/pool-size.filter.ts
Normal file
36
filters/pool-size.filter.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Filter, FilterResult } from './pool-filters';
|
||||||
|
import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
|
||||||
|
import { Connection } from '@solana/web3.js';
|
||||||
|
|
||||||
|
export class PoolSizeFilter implements Filter {
|
||||||
|
constructor(
|
||||||
|
private readonly connection: Connection,
|
||||||
|
private readonly quoteToken: Token,
|
||||||
|
private readonly minPoolSize: TokenAmount,
|
||||||
|
private readonly maxPoolSize: TokenAmount,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
|
||||||
|
const response = await this.connection.getTokenAccountBalance(poolState.quoteVault, this.connection.commitment);
|
||||||
|
const poolSize = new TokenAmount(this.quoteToken, response.value.amount, true);
|
||||||
|
let inRange = true;
|
||||||
|
|
||||||
|
if (!this.maxPoolSize?.isZero()) {
|
||||||
|
inRange = poolSize.lt(this.maxPoolSize);
|
||||||
|
|
||||||
|
if (!inRange) {
|
||||||
|
return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} > ${this.maxPoolSize.toFixed()}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.minPoolSize?.isZero()) {
|
||||||
|
inRange = poolSize.gt(this.minPoolSize);
|
||||||
|
|
||||||
|
if (!inRange) {
|
||||||
|
return { ok: false, message: `PoolSize -> Pool size ${poolSize.toFixed()} < ${this.minPoolSize.toFixed()}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: inRange };
|
||||||
|
}
|
||||||
|
}
|
||||||
26
filters/renounced.filter.ts
Normal file
26
filters/renounced.filter.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Filter, FilterResult } from './pool-filters';
|
||||||
|
import { MintLayout } from '@solana/spl-token';
|
||||||
|
import { Connection } from '@solana/web3.js';
|
||||||
|
import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
|
||||||
|
import { logger } from '../helpers';
|
||||||
|
|
||||||
|
export class RenouncedFilter implements Filter {
|
||||||
|
constructor(private readonly connection: Connection) {}
|
||||||
|
|
||||||
|
async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
|
||||||
|
try {
|
||||||
|
const accountInfo = await this.connection.getAccountInfo(poolState.baseMint, this.connection.commitment);
|
||||||
|
if (!accountInfo?.data) {
|
||||||
|
return { ok: false, message: 'Renounced -> Failed to fetch account data' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const deserialize = MintLayout.decode(accountInfo.data);
|
||||||
|
const renounced = deserialize.mintAuthorityOption === 0;
|
||||||
|
return { ok: renounced, message: renounced ? undefined : 'Renounced -> Creator can mint more tokens' };
|
||||||
|
} catch (e) {
|
||||||
|
logger.error({ mint: poolState.baseMint }, `Failed to check if mint is renounced`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, message: 'Renounced -> Failed to check if mint is renounced' };
|
||||||
|
}
|
||||||
|
}
|
||||||
57
helpers/constants.ts
Normal file
57
helpers/constants.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Logger } from 'pino';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { Commitment } from '@solana/web3.js';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const retrieveEnvVariable = (variableName: string, logger: Logger) => {
|
||||||
|
const variable = process.env[variableName] || '';
|
||||||
|
if (!variable) {
|
||||||
|
logger.error(`${variableName} is not set`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return variable;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wallet
|
||||||
|
export const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
|
||||||
|
|
||||||
|
// Connection
|
||||||
|
export const NETWORK = 'mainnet-beta';
|
||||||
|
export const COMMITMENT_LEVEL: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
|
||||||
|
export const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
|
||||||
|
export const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
|
||||||
|
|
||||||
|
// Bot
|
||||||
|
export const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL', logger);
|
||||||
|
export const ONE_TOKEN_AT_A_TIME = retrieveEnvVariable('ONE_TOKEN_AT_A_TIME', logger) === 'true';
|
||||||
|
export const COMPUTE_UNIT_LIMIT = Number(retrieveEnvVariable('COMPUTE_UNIT_LIMIT', logger));
|
||||||
|
export const COMPUTE_UNIT_PRICE = Number(retrieveEnvVariable('COMPUTE_UNIT_PRICE', logger));
|
||||||
|
export const PRE_LOAD_EXISTING_MARKETS = retrieveEnvVariable('PRE_LOAD_EXISTING_MARKETS', logger) === 'true';
|
||||||
|
export const CACHE_NEW_MARKETS = retrieveEnvVariable('CACHE_NEW_MARKETS', logger) === 'true';
|
||||||
|
|
||||||
|
// Buy
|
||||||
|
export const AUTO_BUY_DELAY = Number(retrieveEnvVariable('AUTO_BUY_DELAY', logger));
|
||||||
|
export const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger);
|
||||||
|
export const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger);
|
||||||
|
export const MAX_BUY_RETRIES = Number(retrieveEnvVariable('MAX_BUY_RETRIES', logger));
|
||||||
|
export const BUY_SLIPPAGE = Number(retrieveEnvVariable('BUY_SLIPPAGE', logger));
|
||||||
|
|
||||||
|
// Sell
|
||||||
|
export const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
|
||||||
|
export const AUTO_SELL_DELAY = Number(retrieveEnvVariable('AUTO_SELL_DELAY', logger));
|
||||||
|
export const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
|
||||||
|
export const TAKE_PROFIT = Number(retrieveEnvVariable('TAKE_PROFIT', logger));
|
||||||
|
export const STOP_LOSS = Number(retrieveEnvVariable('STOP_LOSS', logger));
|
||||||
|
export const PRICE_CHECK_INTERVAL = Number(retrieveEnvVariable('PRICE_CHECK_INTERVAL', logger));
|
||||||
|
export const PRICE_CHECK_DURATION = Number(retrieveEnvVariable('PRICE_CHECK_DURATION', logger));
|
||||||
|
export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger));
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
export const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
|
||||||
|
export const CHECK_IF_BURNED = retrieveEnvVariable('CHECK_IF_BURNED', logger) === 'true';
|
||||||
|
export const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger);
|
||||||
|
export const MAX_POOL_SIZE = retrieveEnvVariable('MAX_POOL_SIZE', logger);
|
||||||
|
export const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
|
||||||
|
export const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
|
||||||
7
helpers/index.ts
Normal file
7
helpers/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './market';
|
||||||
|
export * from './liquidity';
|
||||||
|
export * from './logger';
|
||||||
|
export * from './constants';
|
||||||
|
export * from './token';
|
||||||
|
export * from './wallet';
|
||||||
|
export * from './promises'
|
||||||
@ -1,26 +1,6 @@
|
|||||||
import { Commitment, Connection, PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import {
|
import { Liquidity, LiquidityPoolKeys, LiquidityStateV4, MAINNET_PROGRAM_ID, Market } from '@raydium-io/raydium-sdk';
|
||||||
Liquidity,
|
import { MinimalMarketLayoutV3 } from './market';
|
||||||
LiquidityPoolKeys,
|
|
||||||
Market,
|
|
||||||
TokenAccount,
|
|
||||||
SPL_ACCOUNT_LAYOUT,
|
|
||||||
publicKey,
|
|
||||||
struct,
|
|
||||||
MAINNET_PROGRAM_ID,
|
|
||||||
LiquidityStateV4,
|
|
||||||
} from '@raydium-io/raydium-sdk';
|
|
||||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
||||||
import { MinimalMarketLayoutV3 } from '../market';
|
|
||||||
|
|
||||||
export const RAYDIUM_LIQUIDITY_PROGRAM_ID_V4 = MAINNET_PROGRAM_ID.AmmV4;
|
|
||||||
export const OPENBOOK_PROGRAM_ID = MAINNET_PROGRAM_ID.OPENBOOK_MARKET;
|
|
||||||
|
|
||||||
export const MINIMAL_MARKET_STATE_LAYOUT_V3 = struct([
|
|
||||||
publicKey('eventQueue'),
|
|
||||||
publicKey('bids'),
|
|
||||||
publicKey('asks'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function createPoolKeys(
|
export function createPoolKeys(
|
||||||
id: PublicKey,
|
id: PublicKey,
|
||||||
@ -36,9 +16,9 @@ export function createPoolKeys(
|
|||||||
quoteDecimals: accountData.quoteDecimal.toNumber(),
|
quoteDecimals: accountData.quoteDecimal.toNumber(),
|
||||||
lpDecimals: 5,
|
lpDecimals: 5,
|
||||||
version: 4,
|
version: 4,
|
||||||
programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
programId: MAINNET_PROGRAM_ID.AmmV4,
|
||||||
authority: Liquidity.getAssociatedAuthority({
|
authority: Liquidity.getAssociatedAuthority({
|
||||||
programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
programId: MAINNET_PROGRAM_ID.AmmV4,
|
||||||
}).publicKey,
|
}).publicKey,
|
||||||
openOrders: accountData.openOrders,
|
openOrders: accountData.openOrders,
|
||||||
targetOrders: accountData.targetOrders,
|
targetOrders: accountData.targetOrders,
|
||||||
@ -61,28 +41,3 @@ export function createPoolKeys(
|
|||||||
lookupTableAccount: PublicKey.default,
|
lookupTableAccount: PublicKey.default,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTokenAccounts(
|
|
||||||
connection: Connection,
|
|
||||||
owner: PublicKey,
|
|
||||||
commitment?: Commitment,
|
|
||||||
) {
|
|
||||||
const tokenResp = await connection.getTokenAccountsByOwner(
|
|
||||||
owner,
|
|
||||||
{
|
|
||||||
programId: TOKEN_PROGRAM_ID,
|
|
||||||
},
|
|
||||||
commitment,
|
|
||||||
);
|
|
||||||
|
|
||||||
const accounts: TokenAccount[] = [];
|
|
||||||
for (const { pubkey, account } of tokenResp.value) {
|
|
||||||
accounts.push({
|
|
||||||
pubkey,
|
|
||||||
programId: account.owner,
|
|
||||||
accountInfo: SPL_ACCOUNT_LAYOUT.decode(account.data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return accounts;
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import pino from "pino";
|
import pino from 'pino';
|
||||||
|
|
||||||
const transport = pino.transport({
|
const transport = pino.transport({
|
||||||
target: 'pino-pretty',
|
target: 'pino-pretty',
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import { Commitment, Connection, PublicKey } from '@solana/web3.js';
|
import { Commitment, Connection, PublicKey } from '@solana/web3.js';
|
||||||
import { GetStructureSchema, MARKET_STATE_LAYOUT_V3 } from '@raydium-io/raydium-sdk';
|
import { GetStructureSchema, MARKET_STATE_LAYOUT_V3, publicKey, struct } from '@raydium-io/raydium-sdk';
|
||||||
import { MINIMAL_MARKET_STATE_LAYOUT_V3 } from '../liquidity';
|
|
||||||
|
|
||||||
|
export const MINIMAL_MARKET_STATE_LAYOUT_V3 = struct([publicKey('eventQueue'), publicKey('bids'), publicKey('asks')]);
|
||||||
export type MinimalMarketStateLayoutV3 = typeof MINIMAL_MARKET_STATE_LAYOUT_V3;
|
export type MinimalMarketStateLayoutV3 = typeof MINIMAL_MARKET_STATE_LAYOUT_V3;
|
||||||
export type MinimalMarketLayoutV3 =
|
export type MinimalMarketLayoutV3 = GetStructureSchema<MinimalMarketStateLayoutV3>;
|
||||||
GetStructureSchema<MinimalMarketStateLayoutV3>;
|
|
||||||
|
|
||||||
export async function getMinimalMarketV3(
|
export async function getMinimalMarketV3(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
1
helpers/promises.ts
Normal file
1
helpers/promises.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
23
helpers/token.ts
Normal file
23
helpers/token.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Token } from '@raydium-io/raydium-sdk';
|
||||||
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
|
import { PublicKey } from '@solana/web3.js';
|
||||||
|
|
||||||
|
export function getToken(token: string) {
|
||||||
|
switch (token) {
|
||||||
|
case 'WSOL': {
|
||||||
|
return Token.WSOL;
|
||||||
|
}
|
||||||
|
case 'USDC': {
|
||||||
|
return new Token(
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
|
||||||
|
6,
|
||||||
|
'USDC',
|
||||||
|
'USDC',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported quote mint "${token}". Supported values are USDC and WSOL`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
helpers/wallet.ts
Normal file
21
helpers/wallet.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Keypair } from '@solana/web3.js';
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
import { mnemonicToSeedSync } from 'bip39';
|
||||||
|
import { derivePath } from 'ed25519-hd-key';
|
||||||
|
|
||||||
|
export function getWallet(wallet: string): Keypair {
|
||||||
|
// most likely someone pasted the private key in binary format
|
||||||
|
if (wallet.startsWith('[')) {
|
||||||
|
return Keypair.fromSecretKey(JSON.parse(wallet));
|
||||||
|
}
|
||||||
|
|
||||||
|
// most likely someone pasted mnemonic
|
||||||
|
if (wallet.split(' ').length > 1) {
|
||||||
|
const seed = mnemonicToSeedSync(wallet, '');
|
||||||
|
const path = `m/44'/501'/0'/0'`; // we assume it's first path
|
||||||
|
return Keypair.fromSeed(derivePath(path, seed.toString('hex')).key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// most likely someone pasted base58 encoded private key
|
||||||
|
return Keypair.fromSecretKey(bs58.decode(wallet));
|
||||||
|
}
|
||||||
192
index.ts
Normal file
192
index.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { MarketCache, PoolCache } from './cache';
|
||||||
|
import { Listeners } from './listeners';
|
||||||
|
import { Connection, KeyedAccountInfo, Keypair } from '@solana/web3.js';
|
||||||
|
import { LIQUIDITY_STATE_LAYOUT_V4, MARKET_STATE_LAYOUT_V3, Token, TokenAmount } from '@raydium-io/raydium-sdk';
|
||||||
|
import { AccountLayout, getAssociatedTokenAddressSync } from '@solana/spl-token';
|
||||||
|
import { Bot, BotConfig } from './bot';
|
||||||
|
import { DefaultTransactionExecutor } from './transactions';
|
||||||
|
import {
|
||||||
|
getToken,
|
||||||
|
getWallet,
|
||||||
|
logger,
|
||||||
|
COMMITMENT_LEVEL,
|
||||||
|
RPC_ENDPOINT,
|
||||||
|
RPC_WEBSOCKET_ENDPOINT,
|
||||||
|
PRE_LOAD_EXISTING_MARKETS,
|
||||||
|
LOG_LEVEL,
|
||||||
|
CHECK_IF_MINT_IS_RENOUNCED,
|
||||||
|
CHECK_IF_BURNED,
|
||||||
|
QUOTE_MINT,
|
||||||
|
MAX_POOL_SIZE,
|
||||||
|
MIN_POOL_SIZE,
|
||||||
|
QUOTE_AMOUNT,
|
||||||
|
PRIVATE_KEY,
|
||||||
|
USE_SNIPE_LIST,
|
||||||
|
ONE_TOKEN_AT_A_TIME,
|
||||||
|
AUTO_SELL_DELAY,
|
||||||
|
MAX_SELL_RETRIES,
|
||||||
|
AUTO_SELL,
|
||||||
|
MAX_BUY_RETRIES,
|
||||||
|
AUTO_BUY_DELAY,
|
||||||
|
COMPUTE_UNIT_LIMIT,
|
||||||
|
COMPUTE_UNIT_PRICE,
|
||||||
|
CACHE_NEW_MARKETS,
|
||||||
|
TAKE_PROFIT,
|
||||||
|
STOP_LOSS,
|
||||||
|
BUY_SLIPPAGE,
|
||||||
|
SELL_SLIPPAGE,
|
||||||
|
PRICE_CHECK_DURATION,
|
||||||
|
PRICE_CHECK_INTERVAL, SNIPE_LIST_REFRESH_INTERVAL,
|
||||||
|
} from './helpers';
|
||||||
|
import { version } from './package.json';
|
||||||
|
|
||||||
|
const connection = new Connection(RPC_ENDPOINT, {
|
||||||
|
wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
|
||||||
|
commitment: COMMITMENT_LEVEL,
|
||||||
|
});
|
||||||
|
|
||||||
|
function printDetails(wallet: Keypair, quoteToken: Token, botConfig: BotConfig) {
|
||||||
|
logger.info(`
|
||||||
|
.. :-===++++-
|
||||||
|
.-==+++++++- =+++++++++-
|
||||||
|
..:::--===+=.=: .+++++++++++:=+++++++++:
|
||||||
|
.==+++++++++++++++=:+++: .+++++++++++.=++++++++-.
|
||||||
|
.-+++++++++++++++=:=++++- .+++++++++=:.=+++++-::-.
|
||||||
|
-:+++++++++++++=:+++++++- .++++++++-:- =+++++=-:
|
||||||
|
-:++++++=++++=:++++=++++= .++++++++++- =+++++:
|
||||||
|
-:++++-:=++=:++++=:-+++++:+++++====--:::::::.
|
||||||
|
::=+-:::==:=+++=::-:--::::::::::---------::.
|
||||||
|
::-: .::::::::. --------:::..
|
||||||
|
:- .:.-:::.
|
||||||
|
|
||||||
|
WARP DRIVE ACTIVATED 🚀🐟
|
||||||
|
Made with ❤️ by humans.
|
||||||
|
Version: ${version}
|
||||||
|
`);
|
||||||
|
|
||||||
|
logger.info('------- CONFIGURATION START -------');
|
||||||
|
logger.info(`Wallet: ${wallet.publicKey.toString()}`);
|
||||||
|
|
||||||
|
logger.info('- Bot -');
|
||||||
|
logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
|
||||||
|
logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
|
||||||
|
logger.info(`Single token at the time: ${botConfig.oneTokenAtATime}`);
|
||||||
|
logger.info(`Pre load existing markets: ${PRE_LOAD_EXISTING_MARKETS}`);
|
||||||
|
logger.info(`Cache new markets: ${CACHE_NEW_MARKETS}`);
|
||||||
|
logger.info(`Log level: ${LOG_LEVEL}`);
|
||||||
|
|
||||||
|
logger.info('- Buy -');
|
||||||
|
logger.info(`Buy amount: ${botConfig.quoteAmount.toFixed()} ${botConfig.quoteToken.name}`);
|
||||||
|
logger.info(`Auto buy delay: ${botConfig.autoBuyDelay} ms`);
|
||||||
|
logger.info(`Max buy retries: ${botConfig.maxBuyRetries}`);
|
||||||
|
logger.info(`Buy amount (${quoteToken.symbol}): ${botConfig.quoteAmount.toFixed()}`);
|
||||||
|
logger.info(`Buy slippage: ${botConfig.buySlippage}%`);
|
||||||
|
|
||||||
|
logger.info('- Sell -');
|
||||||
|
logger.info(`Auto sell: ${AUTO_SELL}`);
|
||||||
|
logger.info(`Auto sell delay: ${botConfig.autoSellDelay} ms`);
|
||||||
|
logger.info(`Max sell retries: ${botConfig.maxSellRetries}`);
|
||||||
|
logger.info(`Sell slippage: ${botConfig.sellSlippage}%`);
|
||||||
|
logger.info(`Price check interval: ${botConfig.priceCheckInterval} ms`);
|
||||||
|
logger.info(`Price check duration: ${botConfig.priceCheckDuration} ms`);
|
||||||
|
logger.info(`Take profit: ${botConfig.takeProfit}%`);
|
||||||
|
logger.info(`Stop loss: ${botConfig.stopLoss}%`);
|
||||||
|
|
||||||
|
logger.info('- Filters -');
|
||||||
|
logger.info(`Snipe list: ${botConfig.useSnipeList}`);
|
||||||
|
logger.info(`Snipe list refresh interval: ${SNIPE_LIST_REFRESH_INTERVAL} ms`);
|
||||||
|
logger.info(`Check renounced: ${botConfig.checkRenounced}`);
|
||||||
|
logger.info(`Check burned: ${botConfig.checkBurned}`);
|
||||||
|
logger.info(`Min pool size: ${botConfig.minPoolSize.toFixed()}`);
|
||||||
|
logger.info(`Max pool size: ${botConfig.maxPoolSize.toFixed()}`);
|
||||||
|
|
||||||
|
logger.info('------- CONFIGURATION END -------');
|
||||||
|
|
||||||
|
logger.info('Bot is running! Press CTRL + C to stop it.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runListener = async () => {
|
||||||
|
logger.level = LOG_LEVEL;
|
||||||
|
logger.info('Bot is starting...');
|
||||||
|
|
||||||
|
const marketCache = new MarketCache(connection);
|
||||||
|
const poolCache = new PoolCache();
|
||||||
|
const txExecutor = new DefaultTransactionExecutor(connection);
|
||||||
|
const wallet = getWallet(PRIVATE_KEY.trim());
|
||||||
|
const quoteToken = getToken(QUOTE_MINT);
|
||||||
|
const botConfig = <BotConfig>{
|
||||||
|
wallet,
|
||||||
|
quoteAta: getAssociatedTokenAddressSync(quoteToken.mint, wallet.publicKey),
|
||||||
|
checkRenounced: CHECK_IF_MINT_IS_RENOUNCED,
|
||||||
|
checkBurned: CHECK_IF_BURNED,
|
||||||
|
minPoolSize: new TokenAmount(quoteToken, MIN_POOL_SIZE, false),
|
||||||
|
maxPoolSize: new TokenAmount(quoteToken, MAX_POOL_SIZE, false),
|
||||||
|
quoteToken,
|
||||||
|
quoteAmount: new TokenAmount(quoteToken, QUOTE_AMOUNT, false),
|
||||||
|
oneTokenAtATime: ONE_TOKEN_AT_A_TIME,
|
||||||
|
useSnipeList: USE_SNIPE_LIST,
|
||||||
|
autoSellDelay: AUTO_SELL_DELAY,
|
||||||
|
maxSellRetries: MAX_SELL_RETRIES,
|
||||||
|
autoBuyDelay: AUTO_BUY_DELAY,
|
||||||
|
maxBuyRetries: MAX_BUY_RETRIES,
|
||||||
|
unitLimit: COMPUTE_UNIT_LIMIT,
|
||||||
|
unitPrice: COMPUTE_UNIT_PRICE,
|
||||||
|
takeProfit: TAKE_PROFIT,
|
||||||
|
stopLoss: STOP_LOSS,
|
||||||
|
buySlippage: BUY_SLIPPAGE,
|
||||||
|
sellSlippage: SELL_SLIPPAGE,
|
||||||
|
priceCheckInterval: PRICE_CHECK_INTERVAL,
|
||||||
|
priceCheckDuration: PRICE_CHECK_DURATION,
|
||||||
|
};
|
||||||
|
|
||||||
|
const bot = new Bot(connection, marketCache, poolCache, txExecutor, botConfig);
|
||||||
|
const valid = await bot.validate();
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
logger.info('Bot is exiting...');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PRE_LOAD_EXISTING_MARKETS) {
|
||||||
|
await marketCache.init({ quoteToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
const runTimestamp = Math.floor(new Date().getTime() / 1000);
|
||||||
|
const listeners = new Listeners(connection);
|
||||||
|
await listeners.start({
|
||||||
|
walletPublicKey: wallet.publicKey,
|
||||||
|
quoteToken,
|
||||||
|
autoSell: AUTO_SELL,
|
||||||
|
cacheNewMarkets: CACHE_NEW_MARKETS,
|
||||||
|
});
|
||||||
|
|
||||||
|
listeners.on('market', (updatedAccountInfo: KeyedAccountInfo) => {
|
||||||
|
const marketState = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
|
||||||
|
marketCache.save(updatedAccountInfo.accountId.toString(), marketState);
|
||||||
|
});
|
||||||
|
|
||||||
|
listeners.on('pool', async (updatedAccountInfo: KeyedAccountInfo) => {
|
||||||
|
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
|
||||||
|
const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
|
||||||
|
const exists = await poolCache.get(poolState.baseMint.toString());
|
||||||
|
|
||||||
|
if (!exists && poolOpenTime > runTimestamp) {
|
||||||
|
poolCache.save(updatedAccountInfo.accountId.toString(), poolState);
|
||||||
|
await bot.buy(updatedAccountInfo.accountId, poolState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
listeners.on('wallet', async (updatedAccountInfo: KeyedAccountInfo) => {
|
||||||
|
const accountData = AccountLayout.decode(updatedAccountInfo.accountInfo.data);
|
||||||
|
|
||||||
|
if (accountData.mint.equals(quoteToken.mint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await bot.sell(updatedAccountInfo.accountId, accountData);
|
||||||
|
});
|
||||||
|
|
||||||
|
printDetails(wallet, quoteToken, botConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
runListener();
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './liquidity';
|
|
||||||
1
listeners/index.ts
Normal file
1
listeners/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './listeners';
|
||||||
112
listeners/listeners.ts
Normal file
112
listeners/listeners.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { LIQUIDITY_STATE_LAYOUT_V4, MAINNET_PROGRAM_ID, MARKET_STATE_LAYOUT_V3, Token } from '@raydium-io/raydium-sdk';
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
import { Connection, PublicKey } from '@solana/web3.js';
|
||||||
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export class Listeners extends EventEmitter {
|
||||||
|
private subscriptions: number[] = [];
|
||||||
|
|
||||||
|
constructor(private readonly connection: Connection) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(config: {
|
||||||
|
walletPublicKey: PublicKey;
|
||||||
|
quoteToken: Token;
|
||||||
|
autoSell: boolean;
|
||||||
|
cacheNewMarkets: boolean;
|
||||||
|
}) {
|
||||||
|
if (config.cacheNewMarkets) {
|
||||||
|
const openBookSubscription = await this.subscribeToOpenBookMarkets(config);
|
||||||
|
this.subscriptions.push(openBookSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raydiumSubscription = await this.subscribeToRaydiumPools(config);
|
||||||
|
this.subscriptions.push(raydiumSubscription);
|
||||||
|
|
||||||
|
if (config.autoSell) {
|
||||||
|
const walletSubscription = await this.subscribeToWalletChanges(config);
|
||||||
|
this.subscriptions.push(walletSubscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribeToOpenBookMarkets(config: { quoteToken: Token }) {
|
||||||
|
return this.connection.onProgramAccountChange(
|
||||||
|
MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
|
||||||
|
async (updatedAccountInfo) => {
|
||||||
|
this.emit('market', updatedAccountInfo);
|
||||||
|
},
|
||||||
|
this.connection.commitment,
|
||||||
|
[
|
||||||
|
{ dataSize: MARKET_STATE_LAYOUT_V3.span },
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
|
||||||
|
bytes: config.quoteToken.mint.toBase58(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribeToRaydiumPools(config: { quoteToken: Token }) {
|
||||||
|
return this.connection.onProgramAccountChange(
|
||||||
|
MAINNET_PROGRAM_ID.AmmV4,
|
||||||
|
async (updatedAccountInfo) => {
|
||||||
|
this.emit('pool', updatedAccountInfo);
|
||||||
|
},
|
||||||
|
this.connection.commitment,
|
||||||
|
[
|
||||||
|
{ dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
|
||||||
|
bytes: config.quoteToken.mint.toBase58(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('marketProgramId'),
|
||||||
|
bytes: MAINNET_PROGRAM_ID.OPENBOOK_MARKET.toBase58(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
|
||||||
|
bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribeToWalletChanges(config: { walletPublicKey: PublicKey }) {
|
||||||
|
return this.connection.onProgramAccountChange(
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
async (updatedAccountInfo) => {
|
||||||
|
this.emit('wallet', updatedAccountInfo);
|
||||||
|
},
|
||||||
|
this.connection.commitment,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
dataSize: 165,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memcmp: {
|
||||||
|
offset: 32,
|
||||||
|
bytes: config.walletPublicKey.toBase58(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
for (let i = this.subscriptions.length; i >= 0; --i) {
|
||||||
|
const subscription = this.subscriptions[i];
|
||||||
|
await this.connection.removeAccountChangeListener(subscription);
|
||||||
|
this.subscriptions.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './market';
|
|
||||||
2727
package-lock.json
generated
2727
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -1,18 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "solana-sniper-bot",
|
"name": "solana-sniper-bot",
|
||||||
"author": "Filip Dundjer",
|
"author": "Filip Dundjer",
|
||||||
|
"version": "2.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"buy": "ts-node buy.ts",
|
"start": "ts-node index.ts",
|
||||||
"tsc": "tsc --noEmit"
|
"tsc": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@raydium-io/raydium-sdk": "^1.3.1-beta.47",
|
"@raydium-io/raydium-sdk": "^1.3.1-beta.47",
|
||||||
"@solana/spl-token": "^0.4.0",
|
"@solana/spl-token": "^0.4.0",
|
||||||
"@solana/web3.js": "^1.89.1",
|
"@solana/web3.js": "^1.89.1",
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
"bigint-buffer": "^1.1.5",
|
"bigint-buffer": "^1.1.5",
|
||||||
|
"bip39": "^3.1.0",
|
||||||
"bn.js": "^5.2.1",
|
"bn.js": "^5.2.1",
|
||||||
"bs58": "^5.0.0",
|
"bs58": "^5.0.0",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
|
"ed25519-hd-key": "^1.3.0",
|
||||||
|
"i": "^0.3.7",
|
||||||
|
"npm": "^10.5.2",
|
||||||
"pino": "^8.18.0",
|
"pino": "^8.18.0",
|
||||||
"pino-pretty": "^10.3.1",
|
"pino-pretty": "^10.3.1",
|
||||||
"pino-std-serializers": "^6.2.2"
|
"pino-std-serializers": "^6.2.2"
|
||||||
@ -23,4 +29,4 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
transactions/default-transaction-executor.ts
Normal file
37
transactions/default-transaction-executor.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { BlockhashWithExpiryBlockHeight, Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
|
||||||
|
import { TransactionExecutor } from './transaction-executor.interface';
|
||||||
|
import { logger } from '../helpers';
|
||||||
|
|
||||||
|
export class DefaultTransactionExecutor implements TransactionExecutor {
|
||||||
|
constructor(private readonly connection: Connection) {}
|
||||||
|
|
||||||
|
public async executeAndConfirm(
|
||||||
|
transaction: Transaction | VersionedTransaction,
|
||||||
|
latestBlockhash: BlockhashWithExpiryBlockHeight,
|
||||||
|
): Promise<{ confirmed: boolean; signature: string }> {
|
||||||
|
logger.debug('Executing transaction...');
|
||||||
|
const signature = await this.execute(transaction);
|
||||||
|
|
||||||
|
logger.debug({ signature }, 'Confirming transaction...');
|
||||||
|
return this.confirm(signature, latestBlockhash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async execute(transaction: Transaction | VersionedTransaction) {
|
||||||
|
return this.connection.sendRawTransaction(transaction.serialize(), {
|
||||||
|
preflightCommitment: this.connection.commitment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async confirm(signature: string, latestBlockhash: BlockhashWithExpiryBlockHeight) {
|
||||||
|
const confirmation = await this.connection.confirmTransaction(
|
||||||
|
{
|
||||||
|
signature,
|
||||||
|
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||||
|
blockhash: latestBlockhash.blockhash,
|
||||||
|
},
|
||||||
|
this.connection.commitment,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { confirmed: !confirmation.value.err, signature };
|
||||||
|
}
|
||||||
|
}
|
||||||
2
transactions/index.ts
Normal file
2
transactions/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './default-transaction-executor';
|
||||||
|
export * from './transaction-executor.interface';
|
||||||
8
transactions/transaction-executor.interface.ts
Normal file
8
transactions/transaction-executor.interface.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { BlockhashWithExpiryBlockHeight, Transaction, VersionedTransaction } from '@solana/web3.js';
|
||||||
|
|
||||||
|
export interface TransactionExecutor {
|
||||||
|
executeAndConfirm(
|
||||||
|
transaction: Transaction | VersionedTransaction,
|
||||||
|
latestBlockhash: BlockhashWithExpiryBlockHeight,
|
||||||
|
): Promise<{ confirmed: boolean; signature: string }>;
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './mint';
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { struct, u32, u8 } from '@solana/buffer-layout';
|
|
||||||
import { bool, publicKey, u64 } from '@solana/buffer-layout-utils';
|
|
||||||
import { Commitment, Connection, PublicKey } from '@solana/web3.js';
|
|
||||||
|
|
||||||
/** Information about a mint */
|
|
||||||
export interface Mint {
|
|
||||||
/** Address of the mint */
|
|
||||||
address: PublicKey;
|
|
||||||
/**
|
|
||||||
* Optional authority used to mint new tokens. The mint authority may only be provided during mint creation.
|
|
||||||
* If no mint authority is present then the mint has a fixed supply and no further tokens may be minted.
|
|
||||||
*/
|
|
||||||
mintAuthority: PublicKey | null;
|
|
||||||
/** Total supply of tokens */
|
|
||||||
supply: bigint;
|
|
||||||
/** Number of base 10 digits to the right of the decimal place */
|
|
||||||
decimals: number;
|
|
||||||
/** Is this mint initialized */
|
|
||||||
isInitialized: boolean;
|
|
||||||
/** Optional authority to freeze token accounts */
|
|
||||||
freezeAuthority: PublicKey | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mint as stored by the program */
|
|
||||||
export interface RawMint {
|
|
||||||
mintAuthorityOption: 1 | 0;
|
|
||||||
mintAuthority: PublicKey;
|
|
||||||
supply: bigint;
|
|
||||||
decimals: number;
|
|
||||||
isInitialized: boolean;
|
|
||||||
freezeAuthorityOption: 1 | 0;
|
|
||||||
freezeAuthority: PublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Buffer layout for de/serializing a mint */
|
|
||||||
export const MintLayout = struct<RawMint>([
|
|
||||||
u32('mintAuthorityOption'),
|
|
||||||
publicKey('mintAuthority'),
|
|
||||||
u64('supply'),
|
|
||||||
u8('decimals'),
|
|
||||||
bool('isInitialized'),
|
|
||||||
u32('freezeAuthorityOption'),
|
|
||||||
publicKey('freezeAuthority'),
|
|
||||||
]);
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './utils';
|
|
||||||
export * from './logger';
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { Logger } from 'pino';
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user