fix: cleanup sell function

This commit is contained in:
Filip Dunder
2024-02-20 18:51:22 +01:00
parent 2f0e73c9eb
commit 63d67616ff
2 changed files with 47 additions and 108 deletions

View File

@ -1,4 +1,5 @@
{ {
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all",
"printWidth": 120
} }

136
buy.ts
View File

@ -10,6 +10,7 @@ import {
} from '@raydium-io/raydium-sdk'; } from '@raydium-io/raydium-sdk';
import { import {
createAssociatedTokenAccountIdempotentInstruction, createAssociatedTokenAccountIdempotentInstruction,
createCloseAccountInstruction,
getAssociatedTokenAddressSync, getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID,
} from '@solana/spl-token'; } from '@solana/spl-token';
@ -23,12 +24,7 @@ import {
VersionedTransaction, VersionedTransaction,
Commitment, Commitment,
} from '@solana/web3.js'; } from '@solana/web3.js';
import { import { getTokenAccounts, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, OPENBOOK_PROGRAM_ID, createPoolKeys } from './liquidity';
getTokenAccounts,
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
OPENBOOK_PROGRAM_ID,
createPoolKeys,
} from './liquidity';
import { retrieveEnvVariable } from './utils'; import { retrieveEnvVariable } from './utils';
import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market'; import { getMinimalMarketV3, MinimalMarketLayoutV3 } from './market';
import pino from 'pino'; import pino from 'pino';
@ -39,7 +35,6 @@ import BN from 'bn.js';
const transport = pino.transport({ const transport = pino.transport({
targets: [ targets: [
// { // {
// level: 'trace', // level: 'trace',
// target: 'pino/file', // target: 'pino/file',
@ -69,10 +64,7 @@ export const logger = pino(
const network = 'mainnet-beta'; const network = 'mainnet-beta';
const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger); const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT', logger);
const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable( const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOINT', logger);
'RPC_WEBSOCKET_ENDPOINT',
logger,
);
const solanaConnection = new Connection(RPC_ENDPOINT, { const solanaConnection = new Connection(RPC_ENDPOINT, {
wsEndpoint: RPC_WEBSOCKET_ENDPOINT, wsEndpoint: RPC_WEBSOCKET_ENDPOINT,
@ -87,24 +79,20 @@ export type MinimalTokenAccountData = {
let existingLiquidityPools: Set<string> = new Set<string>(); let existingLiquidityPools: Set<string> = new Set<string>();
let existingOpenBookMarkets: Set<string> = new Set<string>(); let existingOpenBookMarkets: Set<string> = new Set<string>();
let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map< let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<string, MinimalTokenAccountData>();
string,
MinimalTokenAccountData
>();
let wallet: Keypair; let wallet: Keypair;
let quoteToken: Token; let quoteToken: Token;
let quoteTokenAssociatedAddress: PublicKey; let quoteTokenAssociatedAddress: PublicKey;
let quoteAmount: TokenAmount; let quoteAmount: TokenAmount;
let commitment: Commitment = retrieveEnvVariable( let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
'COMMITMENT_LEVEL',
logger,
) as Commitment;
const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true'; const USE_SNIPE_LIST = retrieveEnvVariable('USE_SNIPE_LIST', logger) === 'true';
const SNIPE_LIST_REFRESH_INTERVAL = Number( const SNIPE_LIST_REFRESH_INTERVAL = Number(retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger));
retrieveEnvVariable('SNIPE_LIST_REFRESH_INTERVAL', logger), const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger) === 'true';
); const SELL_DELAY = Number(retrieveEnvVariable('SELL_DELAY', logger));
const MAX_SELL_RETRIES = 60;
let snipeList: string[] = []; let snipeList: string[] = [];
async function init(): Promise<void> { async function init(): Promise<void> {
@ -134,9 +122,7 @@ async function init(): Promise<void> {
break; break;
} }
default: { default: {
throw new Error( throw new Error(`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`);
`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`,
);
} }
} }
@ -145,29 +131,19 @@ async function init(): Promise<void> {
); );
// check existing wallet for associated token account of quote mint // check existing wallet for associated token account of quote mint
const tokenAccounts = await getTokenAccounts( const tokenAccounts = await getTokenAccounts(solanaConnection, wallet.publicKey, commitment);
solanaConnection,
wallet.publicKey,
commitment,
);
for (const ta of tokenAccounts) { for (const ta of tokenAccounts) {
existingTokenAccounts.set(ta.accountInfo.mint.toString(), < existingTokenAccounts.set(ta.accountInfo.mint.toString(), <MinimalTokenAccountData>{
MinimalTokenAccountData
>{
mint: ta.accountInfo.mint, mint: ta.accountInfo.mint,
address: ta.pubkey, address: ta.pubkey,
}); });
} }
const tokenAccount = tokenAccounts.find( const tokenAccount = tokenAccounts.find((acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString())!;
(acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString(),
)!;
if (!tokenAccount) { if (!tokenAccount) {
throw new Error( throw new Error(`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`);
`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`,
);
} }
quoteTokenAssociatedAddress = tokenAccount.pubkey; quoteTokenAssociatedAddress = tokenAccount.pubkey;
@ -191,10 +167,7 @@ function saveTokenAccount(mint: PublicKey, accountData: MinimalMarketLayoutV3) {
return tokenAccount; return tokenAccount;
} }
export async function processRaydiumPool( export async function processRaydiumPool(id: PublicKey, poolState: LiquidityStateV4) {
id: PublicKey,
poolState: LiquidityStateV4,
) {
try { try {
if (!shouldBuy(poolState.baseMint.toString())) { if (!shouldBuy(poolState.baseMint.toString())) {
return; return;
@ -202,30 +175,20 @@ export async function processRaydiumPool(
await buy(id, poolState); await buy(id, poolState);
if (AUTO_SELL) {
const AUTO_SELL = retrieveEnvVariable('AUTO_SELL', logger); await new Promise((resolve) => setTimeout(resolve, SELL_DELAY));
if (AUTO_SELL === 'true') { const poolKeys = existingTokenAccounts.get(poolState.baseMint.toString())!.poolKeys;
// wait for a bit before selling await sell(poolState, poolKeys as LiquidityPoolKeys);
const SELL_DELAY = retrieveEnvVariable('SELL_DELAY', logger);
const timeout = parseInt(SELL_DELAY, 10);
await new Promise((resolve) => setTimeout(resolve, timeout));
let poolKeys = existingTokenAccounts.get(poolState.baseMint.toString())!.poolKeys;
await sell(id, poolState, poolKeys as LiquidityPoolKeys);
} }
} catch (e) { } catch (e) {
logger.error({ ...poolState, error: e }, `Failed to process pool`); logger.error({ ...poolState, error: e }, `Failed to process pool`);
} }
} }
export async function processOpenBookMarket( export async function processOpenBookMarket(updatedAccountInfo: KeyedAccountInfo) {
updatedAccountInfo: KeyedAccountInfo,
) {
let accountData: MarketStateV3 | undefined; let accountData: MarketStateV3 | undefined;
try { try {
accountData = MARKET_STATE_LAYOUT_V3.decode( accountData = MARKET_STATE_LAYOUT_V3.decode(updatedAccountInfo.accountInfo.data);
updatedAccountInfo.accountInfo.data,
);
// to be competitive, we collect market data before buying the token... // to be competitive, we collect market data before buying the token...
if (existingTokenAccounts.has(accountData.baseMint.toString())) { if (existingTokenAccounts.has(accountData.baseMint.toString())) {
@ -238,27 +201,16 @@ export async function processOpenBookMarket(
} }
} }
async function buy( async function buy(accountId: PublicKey, accountData: LiquidityStateV4): Promise<void> {
accountId: PublicKey,
accountData: LiquidityStateV4,
): Promise<void> {
let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString()); let tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
if (!tokenAccount) { if (!tokenAccount) {
// it's possible that we didn't have time to fetch open book data // it's possible that we didn't have time to fetch open book data
const market = await getMinimalMarketV3( const market = await getMinimalMarketV3(solanaConnection, accountData.marketId, commitment);
solanaConnection,
accountData.marketId,
commitment,
);
tokenAccount = saveTokenAccount(accountData.baseMint, market); tokenAccount = saveTokenAccount(accountData.baseMint, market);
} }
tokenAccount.poolKeys = createPoolKeys( tokenAccount.poolKeys = createPoolKeys(accountId, accountData, tokenAccount.market!);
accountId,
accountData,
tokenAccount.market!,
);
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction( const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
{ {
poolKeys: tokenAccount.poolKeys, poolKeys: tokenAccount.poolKeys,
@ -293,13 +245,10 @@ async function buy(
}).compileToV0Message(); }).compileToV0Message();
const transaction = new VersionedTransaction(messageV0); const transaction = new VersionedTransaction(messageV0);
transaction.sign([wallet, ...innerTransaction.signers]); transaction.sign([wallet, ...innerTransaction.signers]);
const signature = await solanaConnection.sendRawTransaction( const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
transaction.serialize(),
{
maxRetries: 20, maxRetries: 20,
preflightCommitment: commitment, preflightCommitment: commitment,
}, });
);
logger.info( logger.info(
{ {
mint: accountData.baseMint, mint: accountData.baseMint,
@ -309,24 +258,19 @@ async function buy(
); );
} }
const maxRetries = 60; async function sell(accountData: LiquidityStateV4, poolKeys: LiquidityPoolKeys): Promise<void> {
async function sell( const tokenAccount = existingTokenAccounts.get(accountData.baseMint.toString());
accountId: PublicKey,
accountData: LiquidityStateV4,
poolKeys: LiquidityPoolKeys,
): Promise<void> {
const tokenAccount = existingTokenAccounts.get(
accountData.baseMint.toString(),
);
if (!tokenAccount) { if (!tokenAccount) {
return; return;
} }
let retries = 0; let retries = 0;
let balanceFound = false; let balanceFound = false;
while (retries < maxRetries) { while (retries < MAX_SELL_RETRIES) {
try { try {
const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount; const balanceResponse = (await solanaConnection.getTokenAccountBalance(tokenAccount.address)).value.amount;
if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) { if (balanceResponse !== null && Number(balanceResponse) > 0 && !balanceFound) {
balanceFound = true; balanceFound = true;
@ -353,18 +297,16 @@ async function sell(
instructions: [ instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 200000 }), ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 200000 }),
createCloseAccountInstruction(tokenAccount.address, wallet.publicKey, wallet.publicKey),
...innerTransaction.instructions, ...innerTransaction.instructions,
], ],
}).compileToV0Message(); }).compileToV0Message();
const transaction = new VersionedTransaction(messageV0); const transaction = new VersionedTransaction(messageV0);
transaction.sign([wallet, ...innerTransaction.signers]); transaction.sign([wallet, ...innerTransaction.signers]);
const signature = await solanaConnection.sendRawTransaction( const signature = await solanaConnection.sendRawTransaction(transaction.serialize(), {
transaction.serialize(),
{
maxRetries: 5, maxRetries: 5,
preflightCommitment: commitment, preflightCommitment: commitment,
}, });
);
logger.info( logger.info(
{ {
mint: accountData.baseMint, mint: accountData.baseMint,
@ -375,14 +317,13 @@ async function sell(
break; break;
} }
} catch (error) { } catch (error) {
// logger.error(`Error while selling: ${error}`); // ignored
} }
retries++; retries++;
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
} }
} }
function loadSnipeList() { function loadSnipeList() {
if (!USE_SNIPE_LIST) { if (!USE_SNIPE_LIST) {
return; return;
@ -411,9 +352,7 @@ const runListener = async () => {
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
async (updatedAccountInfo) => { async (updatedAccountInfo) => {
const key = updatedAccountInfo.accountId.toString(); const key = updatedAccountInfo.accountId.toString();
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode( const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(updatedAccountInfo.accountInfo.data);
updatedAccountInfo.accountInfo.data,
);
const poolOpenTime = parseInt(poolState.poolOpenTime.toString()); const poolOpenTime = parseInt(poolState.poolOpenTime.toString());
const existing = existingLiquidityPools.has(key); const existing = existingLiquidityPools.has(key);
@ -471,7 +410,6 @@ const runListener = async () => {
logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`); logger.info(`Listening for raydium changes: ${raydiumSubscriptionId}`);
logger.info(`Listening for open book changes: ${openBookSubscriptionId}`); logger.info(`Listening for open book changes: ${openBookSubscriptionId}`);
if (USE_SNIPE_LIST) { if (USE_SNIPE_LIST) {
setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL); setInterval(loadSnipeList, SNIPE_LIST_REFRESH_INTERVAL);
} }