feat: add support for wsol swaps

This commit is contained in:
Filip Dunder
2024-01-30 09:57:39 +01:00
parent e8c8147bae
commit 636ef3e515
10 changed files with 252 additions and 127 deletions

221
buy.ts
View File

@ -1,10 +1,19 @@
import {
Liquidity,
LIQUIDITY_STATE_LAYOUT_V4,
LiquidityPoolKeys,
LiquidityStateV4,
MARKET_STATE_LAYOUT_V2,
MARKET_STATE_LAYOUT_V3,
MarketStateV3,
Token,
TokenAmount,
} from '@raydium-io/raydium-sdk';
import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
import {
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import {
Keypair,
Connection,
@ -13,17 +22,17 @@ import {
KeyedAccountInfo,
TransactionMessage,
VersionedTransaction,
Commitment,
} from '@solana/web3.js';
import {
getAllAccountsV4,
getTokenAccounts,
getAccountPoolKeysFromAccountDataV4,
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
OPENBOOK_PROGRAM_ID,
createPoolKeys,
} from './liquidity';
import { retry, retrieveEnvVariable } from './utils';
import { USDC_AMOUNT, USDC_TOKEN_ID } from './common';
import { getAllMarketsV3 } from './market';
import { retrieveEnvVariable } from './utils';
import { getAllMarketsV3, MinimalMarketLayoutV3 } from './market';
import pino from 'pino';
import bs58 from 'bs58';
@ -71,6 +80,9 @@ const solanaConnection = new Connection(RPC_ENDPOINT, {
export type MinimalTokenAccountData = {
mint: PublicKey;
address: PublicKey;
ata: PublicKey;
poolKeys?: LiquidityPoolKeys;
market?: MinimalMarketLayoutV3;
};
let existingLiquidityPools: Set<string> = new Set<string>();
@ -81,36 +93,95 @@ let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<
>();
let wallet: Keypair;
let usdcTokenKey: PublicKey;
const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
let quoteToken: Token;
let quoteTokenAssociatedAddress: PublicKey;
let quoteAmount: TokenAmount;
let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
async function init(): Promise<void> {
// get wallet
const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));
logger.info(`Wallet Address: ${wallet.publicKey.toString()}`);
const allLiquidityPools = await getAllAccountsV4(solanaConnection);
logger.info(`Wallet Address: ${wallet.publicKey}`);
// get quote mint and amount
const QUOTE_MINT = retrieveEnvVariable('QUOTE_MINT', logger);
const QUOTE_AMOUNT = retrieveEnvVariable('QUOTE_AMOUNT', logger);
switch (QUOTE_MINT) {
case 'WSOL': {
quoteToken = Token.WSOL;
quoteAmount = new TokenAmount(Token.WSOL, QUOTE_AMOUNT, false);
break;
}
case 'USDC': {
quoteToken = new Token(
TOKEN_PROGRAM_ID,
new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
6,
'USDC',
'USDC',
);
quoteAmount = new TokenAmount(quoteToken, QUOTE_AMOUNT, false);
break;
}
default: {
throw new Error(
`Unsupported quote mint "${QUOTE_MINT}". Supported values are USDC and WSOL`,
);
}
}
logger.info(
`Script will buy all new tokens using ${QUOTE_MINT}. Amount that will be used to buy each token is: ${quoteAmount.toFixed().toString()}`
);
// get all existing liquidity pools
const allLiquidityPools = await getAllAccountsV4(
solanaConnection,
quoteToken.mint,
commitment,
);
existingLiquidityPools = new Set(
allLiquidityPools.map((p) => p.id.toString()),
);
const allMarkets = await getAllMarketsV3(solanaConnection);
// get all open-book markets
const allMarkets = await getAllMarketsV3(solanaConnection, quoteToken.mint, commitment);
existingOpenBookMarkets = new Set(allMarkets.map((p) => p.id.toString()));
const tokenAccounts = await getTokenAccounts(
solanaConnection,
wallet.publicKey,
commitment,
);
logger.info(`Total USDC markets ${existingOpenBookMarkets.size}`);
logger.info(`Total USDC pools ${existingLiquidityPools.size}`);
tokenAccounts.forEach((ta) => {
logger.info(
`Total ${quoteToken.symbol} markets ${existingOpenBookMarkets.size}`,
);
logger.info(
`Total ${quoteToken.symbol} pools ${existingLiquidityPools.size}`,
);
// check existing wallet for associated token account of quote mint
for (const ta of tokenAccounts) {
existingTokenAccounts.set(ta.accountInfo.mint.toString(), <
MinimalTokenAccountData
>{
mint: ta.accountInfo.mint,
address: ta.pubkey,
});
});
const token = tokenAccounts.find(
(acc) => acc.accountInfo.mint.toString() === USDC_TOKEN_ID.toString(),
}
const tokenAccount = tokenAccounts.find(
(acc) => acc.accountInfo.mint.toString() === quoteToken.mint.toString(),
)!;
usdcTokenKey = token!.pubkey;
if (!tokenAccount) {
throw new Error(
`No ${quoteToken.symbol} token account found in wallet: ${wallet.publicKey}`,
);
}
quoteTokenAssociatedAddress = tokenAccount.pubkey;
}
export async function processRaydiumPool(updatedAccountInfo: KeyedAccountInfo) {
@ -128,92 +199,98 @@ export async function processRaydiumPool(updatedAccountInfo: KeyedAccountInfo) {
export async function processOpenBookMarket(
updatedAccountInfo: KeyedAccountInfo,
) {
let accountData: any;
let accountData: MarketStateV3 | undefined;
try {
accountData = MARKET_STATE_LAYOUT_V2.decode(
accountData = MARKET_STATE_LAYOUT_V3.decode(
updatedAccountInfo.accountInfo.data,
);
// to be competitive, we create token account before buying the token...
// to be competitive, we collect market data before buying the token...
if (existingTokenAccounts.has(accountData.baseMint.toString())) {
return;
}
const destinationAccount = await getOrCreateAssociatedTokenAccount(
solanaConnection,
wallet,
const ata = getAssociatedTokenAddressSync(
accountData.baseMint,
wallet.publicKey,
);
existingTokenAccounts.set(accountData.baseMint.toString(), <
MinimalTokenAccountData
>{
address: destinationAccount.address,
mint: destinationAccount.mint,
address: ata,
mint: accountData.baseMint,
market: <MinimalMarketLayoutV3>{
bids: accountData.bids,
asks: accountData.asks,
eventQueue: accountData.eventQueue,
},
});
logger.info(
accountData,
`Created destination account: ${destinationAccount.address}`,
);
} catch (e) {
logger.error({ ...accountData, error: e }, `Failed to process market`);
}
}
async function buy(accountId: PublicKey, accountData: any): Promise<void> {
const [poolKeys, latestBlockhash] = await Promise.all([
getAccountPoolKeysFromAccountDataV4(
solanaConnection,
accountId,
accountData,
),
solanaConnection.getLatestBlockhash({ commitment: 'processed' }),
]);
const baseMint = poolKeys.baseMint.toString();
const tokenAccountOut =
existingTokenAccounts && existingTokenAccounts.get(baseMint)?.address;
if (!tokenAccountOut) {
logger.info(`No token account for ${baseMint}`);
return;
}
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
{
poolKeys,
userKeys: {
tokenAccountIn: usdcTokenKey,
tokenAccountOut: tokenAccountOut,
owner: wallet.publicKey,
},
amountIn: USDC_AMOUNT * 1000000,
minAmountOut: 0,
},
poolKeys.version,
async function buy(
accountId: PublicKey,
accountData: LiquidityStateV4,
): Promise<void> {
const tokenAccount = existingTokenAccounts.get(
accountData.baseMint.toString(),
);
if (!tokenAccount) {
return;
}
tokenAccount.poolKeys = createPoolKeys(
accountId,
accountData,
tokenAccount.market!,
);
const { innerTransaction, address } = 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,
});
const messageV0 = new TransactionMessage({
payerKey: wallet.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }),
createAssociatedTokenAccountIdempotentInstruction(
wallet.publicKey,
tokenAccount.address,
wallet.publicKey,
accountData.baseMint,
),
...innerTransaction.instructions,
],
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
transaction.sign([wallet, ...innerTransaction.signers]);
const rawTransaction = transaction.serialize();
const signature = await retry(
() =>
solanaConnection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
}),
{ retryIntervalMs: 10, retries: 50 }, // TODO handle retries more efficiently
const signature = await solanaConnection.sendRawTransaction(
transaction.serialize(),
{
maxRetries: 5,
preflightCommitment: commitment,
},
);
logger.info(
{
...accountData,
mint: accountData.baseMint,
url: `https://solscan.io/tx/${signature}?cluster=${network}`,
},
'Buy',
@ -233,13 +310,13 @@ const runListener = async () => {
const _ = processRaydiumPool(updatedAccountInfo);
}
},
'processed',
commitment,
[
{ dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
{
memcmp: {
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
bytes: USDC_TOKEN_ID.toBase58(),
bytes: quoteToken.mint.toBase58(),
},
},
{
@ -251,7 +328,7 @@ const runListener = async () => {
{
memcmp: {
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
bytes: '14421D35quxec7'
bytes: bs58.encode([6, 0, 0, 0, 0, 0, 0, 0]),
},
},
],
@ -268,13 +345,13 @@ const runListener = async () => {
const _ = processOpenBookMarket(updatedAccountInfo);
}
},
'processed',
commitment,
[
{ dataSize: MARKET_STATE_LAYOUT_V2.span },
{
memcmp: {
offset: MARKET_STATE_LAYOUT_V2.offsetOf('quoteMint'),
bytes: USDC_TOKEN_ID.toBase58(),
bytes: quoteToken.mint.toBase58(),
},
},
],