mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-10 04:22:05 +10:00
Merge pull request #18 from fdundjer/feature/add-suppport-for-wsol
feat: add support for wsol swaps
This commit is contained in:
@ -1,4 +1,6 @@
|
|||||||
PRIVATE_KEY=
|
PRIVATE_KEY=
|
||||||
RPC_ENDPOINT=
|
RPC_ENDPOINT=https://api.mainnet-beta.solana.com
|
||||||
RPC_WEBSOCKET_ENDPOINT=
|
RPC_WEBSOCKET_ENDPOINT=wss://api.mainnet-beta.solana.com
|
||||||
```
|
QUOTE_MINT=WSOL
|
||||||
|
QUOTE_AMOUNT=0.1
|
||||||
|
COMMITMENT_LEVEL=finalized
|
||||||
41
README.md
41
README.md
@ -1,17 +1,44 @@
|
|||||||
# Solana Sniper Bot
|
# Solana Sniper Bot
|
||||||
Proof of concept - 2023-04-20
|
|
||||||
|
|
||||||
This code is written as proof of concept for demonstrating how we can buy new tokens immediately after liquidity pool is created.
|
This code is written as proof of concept for demonstrating how we can buy new tokens immediately after liquidity pool is created.
|
||||||
|
|
||||||
Script listens to new raydium USDC pools and buys token for a fixed amount in USDC.
|
Script listens to new raydium USDC/SOL pools and buys token for a fixed amount in USDC/SOL.
|
||||||
Depending on speed of RPC node, the purchase usually happens before token is available on Raydium for swapping.
|
Depending on speed of RPC node, the purchase usually happens before token is available on Raydium UI for swapping.
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
In order to run the script you need to:
|
In order to run the script you need to:
|
||||||
- Create a new empty Solana wallet
|
- Create a new empty Solana wallet
|
||||||
- Transfer some SOL to it.
|
- Transfer some SOL to it.
|
||||||
- Convert some SOL to USDC.
|
- Convert some SOL to USDC or WSOL.
|
||||||
- We need USDC because the script is buying USDC pairs.
|
- You need USDC or WSOL depending on configuration set below.
|
||||||
- Set your PRIVATE_KEY, RPC_ENDPOINT and RPC_WEBSOCKET_ENDPOINT in the .env file (remove the .copy from the file name when done)
|
- Set your
|
||||||
|
- PRIVATE_KEY (your wallet private key)
|
||||||
|
- RPC_ENDPOINT (https endpoint like helius/quicknode)
|
||||||
|
- RPC_WEBSOCKET_ENDPOINT (websocket endpoint like helius/quicknode)
|
||||||
|
- QUOTE_MINT (which pools to look at, USDC or WSOL)
|
||||||
|
- QUOTE_AMOUNT (amount used to buy each new token)
|
||||||
|
- COMMITMENT_LEVEL
|
||||||
|
|
||||||
|
in the .env file (remove the .copy from the file name when done).
|
||||||
|
Make sure to replace default values.
|
||||||
|
|
||||||
- Install dependencies by typing: `npm install`
|
- Install dependencies by typing: `npm install`
|
||||||
- Run the script by typing: `npm run buy` in terminal
|
- Run the script by typing: `npm run buy` in terminal
|
||||||
|
|
||||||
|
You should see following output:
|
||||||
|

|
||||||
|
|
||||||
|
# Support
|
||||||
|
|
||||||
|
## Unsupported RPC node
|
||||||
|
- If you see following error in your log file:
|
||||||
|
`Error: 410 Gone: {"jsonrpc":"2.0","error":{"code": 410, "message":"The RPC call or parameters have been disabled."}, "id": "986f3599-b2b7-47c4-b951-074c19842bad" }`
|
||||||
|
it means your RPC node doesn't support methods needed to execute script.
|
||||||
|
- FIX: Change your RPC node. You can use Helius or Quicknode.
|
||||||
|
|
||||||
|
|
||||||
|
- If you see following error in your log file:
|
||||||
|
`Error: No SOL token account found in wallet: `
|
||||||
|
it means that wallet you provided doesn't have USDC/WSOL token account.
|
||||||
|
- FIX: Go to dex and swap some SOL to USDC/WSOL. For example when you swap sol to wsol you should see it in wallet as shown below:
|
||||||
|

|
||||||
|
|
||||||
|
|||||||
221
buy.ts
221
buy.ts
@ -1,10 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
Liquidity,
|
Liquidity,
|
||||||
LIQUIDITY_STATE_LAYOUT_V4,
|
LIQUIDITY_STATE_LAYOUT_V4,
|
||||||
|
LiquidityPoolKeys,
|
||||||
LiquidityStateV4,
|
LiquidityStateV4,
|
||||||
MARKET_STATE_LAYOUT_V2,
|
MARKET_STATE_LAYOUT_V2,
|
||||||
|
MARKET_STATE_LAYOUT_V3,
|
||||||
|
MarketStateV3,
|
||||||
|
Token,
|
||||||
|
TokenAmount,
|
||||||
} from '@raydium-io/raydium-sdk';
|
} from '@raydium-io/raydium-sdk';
|
||||||
import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
|
import {
|
||||||
|
createAssociatedTokenAccountIdempotentInstruction,
|
||||||
|
getAssociatedTokenAddressSync,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
} from '@solana/spl-token';
|
||||||
import {
|
import {
|
||||||
Keypair,
|
Keypair,
|
||||||
Connection,
|
Connection,
|
||||||
@ -13,17 +22,17 @@ import {
|
|||||||
KeyedAccountInfo,
|
KeyedAccountInfo,
|
||||||
TransactionMessage,
|
TransactionMessage,
|
||||||
VersionedTransaction,
|
VersionedTransaction,
|
||||||
|
Commitment,
|
||||||
} from '@solana/web3.js';
|
} from '@solana/web3.js';
|
||||||
import {
|
import {
|
||||||
getAllAccountsV4,
|
getAllAccountsV4,
|
||||||
getTokenAccounts,
|
getTokenAccounts,
|
||||||
getAccountPoolKeysFromAccountDataV4,
|
|
||||||
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
||||||
OPENBOOK_PROGRAM_ID,
|
OPENBOOK_PROGRAM_ID,
|
||||||
|
createPoolKeys,
|
||||||
} from './liquidity';
|
} from './liquidity';
|
||||||
import { retry, retrieveEnvVariable } from './utils';
|
import { retrieveEnvVariable } from './utils';
|
||||||
import { USDC_AMOUNT, USDC_TOKEN_ID } from './common';
|
import { getAllMarketsV3, MinimalMarketLayoutV3 } from './market';
|
||||||
import { getAllMarketsV3 } from './market';
|
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import bs58 from 'bs58';
|
import bs58 from 'bs58';
|
||||||
|
|
||||||
@ -71,6 +80,9 @@ const solanaConnection = new Connection(RPC_ENDPOINT, {
|
|||||||
export type MinimalTokenAccountData = {
|
export type MinimalTokenAccountData = {
|
||||||
mint: PublicKey;
|
mint: PublicKey;
|
||||||
address: PublicKey;
|
address: PublicKey;
|
||||||
|
ata: PublicKey;
|
||||||
|
poolKeys?: LiquidityPoolKeys;
|
||||||
|
market?: MinimalMarketLayoutV3;
|
||||||
};
|
};
|
||||||
|
|
||||||
let existingLiquidityPools: Set<string> = new Set<string>();
|
let existingLiquidityPools: Set<string> = new Set<string>();
|
||||||
@ -81,36 +93,95 @@ let existingTokenAccounts: Map<string, MinimalTokenAccountData> = new Map<
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
let wallet: Keypair;
|
let wallet: Keypair;
|
||||||
let usdcTokenKey: PublicKey;
|
let quoteToken: Token;
|
||||||
const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
|
let quoteTokenAssociatedAddress: PublicKey;
|
||||||
|
let quoteAmount: TokenAmount;
|
||||||
|
let commitment: Commitment = retrieveEnvVariable('COMMITMENT_LEVEL', logger) as Commitment;
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
|
// get wallet
|
||||||
|
const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY', logger);
|
||||||
wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));
|
wallet = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY));
|
||||||
logger.info(`Wallet Address: ${wallet.publicKey.toString()}`);
|
logger.info(`Wallet Address: ${wallet.publicKey}`);
|
||||||
const allLiquidityPools = await getAllAccountsV4(solanaConnection);
|
|
||||||
|
// 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(
|
existingLiquidityPools = new Set(
|
||||||
allLiquidityPools.map((p) => p.id.toString()),
|
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()));
|
existingOpenBookMarkets = new Set(allMarkets.map((p) => p.id.toString()));
|
||||||
const tokenAccounts = await getTokenAccounts(
|
const tokenAccounts = await getTokenAccounts(
|
||||||
solanaConnection,
|
solanaConnection,
|
||||||
wallet.publicKey,
|
wallet.publicKey,
|
||||||
|
commitment,
|
||||||
);
|
);
|
||||||
logger.info(`Total USDC markets ${existingOpenBookMarkets.size}`);
|
|
||||||
logger.info(`Total USDC pools ${existingLiquidityPools.size}`);
|
logger.info(
|
||||||
tokenAccounts.forEach((ta) => {
|
`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(), <
|
existingTokenAccounts.set(ta.accountInfo.mint.toString(), <
|
||||||
MinimalTokenAccountData
|
MinimalTokenAccountData
|
||||||
>{
|
>{
|
||||||
mint: ta.accountInfo.mint,
|
mint: ta.accountInfo.mint,
|
||||||
address: ta.pubkey,
|
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) {
|
export async function processRaydiumPool(updatedAccountInfo: KeyedAccountInfo) {
|
||||||
@ -128,92 +199,98 @@ export async function processRaydiumPool(updatedAccountInfo: KeyedAccountInfo) {
|
|||||||
export async function processOpenBookMarket(
|
export async function processOpenBookMarket(
|
||||||
updatedAccountInfo: KeyedAccountInfo,
|
updatedAccountInfo: KeyedAccountInfo,
|
||||||
) {
|
) {
|
||||||
let accountData: any;
|
let accountData: MarketStateV3 | undefined;
|
||||||
try {
|
try {
|
||||||
accountData = MARKET_STATE_LAYOUT_V2.decode(
|
accountData = MARKET_STATE_LAYOUT_V3.decode(
|
||||||
updatedAccountInfo.accountInfo.data,
|
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())) {
|
if (existingTokenAccounts.has(accountData.baseMint.toString())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationAccount = await getOrCreateAssociatedTokenAccount(
|
const ata = getAssociatedTokenAddressSync(
|
||||||
solanaConnection,
|
|
||||||
wallet,
|
|
||||||
accountData.baseMint,
|
accountData.baseMint,
|
||||||
wallet.publicKey,
|
wallet.publicKey,
|
||||||
);
|
);
|
||||||
existingTokenAccounts.set(accountData.baseMint.toString(), <
|
existingTokenAccounts.set(accountData.baseMint.toString(), <
|
||||||
MinimalTokenAccountData
|
MinimalTokenAccountData
|
||||||
>{
|
>{
|
||||||
address: destinationAccount.address,
|
address: ata,
|
||||||
mint: destinationAccount.mint,
|
mint: accountData.baseMint,
|
||||||
|
market: <MinimalMarketLayoutV3>{
|
||||||
|
bids: accountData.bids,
|
||||||
|
asks: accountData.asks,
|
||||||
|
eventQueue: accountData.eventQueue,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
logger.info(
|
|
||||||
accountData,
|
|
||||||
`Created destination account: ${destinationAccount.address}`,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error({ ...accountData, error: e }, `Failed to process market`);
|
logger.error({ ...accountData, error: e }, `Failed to process market`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buy(accountId: PublicKey, accountData: any): Promise<void> {
|
async function buy(
|
||||||
const [poolKeys, latestBlockhash] = await Promise.all([
|
accountId: PublicKey,
|
||||||
getAccountPoolKeysFromAccountDataV4(
|
accountData: LiquidityStateV4,
|
||||||
solanaConnection,
|
): Promise<void> {
|
||||||
accountId,
|
const tokenAccount = existingTokenAccounts.get(
|
||||||
accountData,
|
accountData.baseMint.toString(),
|
||||||
),
|
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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({
|
const messageV0 = new TransactionMessage({
|
||||||
payerKey: wallet.publicKey,
|
payerKey: wallet.publicKey,
|
||||||
recentBlockhash: latestBlockhash.blockhash,
|
recentBlockhash: latestBlockhash.blockhash,
|
||||||
instructions: [
|
instructions: [
|
||||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
|
ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }),
|
||||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }),
|
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30000 }),
|
||||||
|
createAssociatedTokenAccountIdempotentInstruction(
|
||||||
|
wallet.publicKey,
|
||||||
|
tokenAccount.address,
|
||||||
|
wallet.publicKey,
|
||||||
|
accountData.baseMint,
|
||||||
|
),
|
||||||
...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 rawTransaction = transaction.serialize();
|
const signature = await solanaConnection.sendRawTransaction(
|
||||||
const signature = await retry(
|
transaction.serialize(),
|
||||||
() =>
|
{
|
||||||
solanaConnection.sendRawTransaction(rawTransaction, {
|
maxRetries: 5,
|
||||||
skipPreflight: true,
|
preflightCommitment: commitment,
|
||||||
}),
|
},
|
||||||
{ retryIntervalMs: 10, retries: 50 }, // TODO handle retries more efficiently
|
|
||||||
);
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
...accountData,
|
mint: accountData.baseMint,
|
||||||
url: `https://solscan.io/tx/${signature}?cluster=${network}`,
|
url: `https://solscan.io/tx/${signature}?cluster=${network}`,
|
||||||
},
|
},
|
||||||
'Buy',
|
'Buy',
|
||||||
@ -233,13 +310,13 @@ const runListener = async () => {
|
|||||||
const _ = processRaydiumPool(updatedAccountInfo);
|
const _ = processRaydiumPool(updatedAccountInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'processed',
|
commitment,
|
||||||
[
|
[
|
||||||
{ dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
|
{ dataSize: LIQUIDITY_STATE_LAYOUT_V4.span },
|
||||||
{
|
{
|
||||||
memcmp: {
|
memcmp: {
|
||||||
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
|
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
|
||||||
bytes: USDC_TOKEN_ID.toBase58(),
|
bytes: quoteToken.mint.toBase58(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -251,7 +328,7 @@ const runListener = async () => {
|
|||||||
{
|
{
|
||||||
memcmp: {
|
memcmp: {
|
||||||
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('status'),
|
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);
|
const _ = processOpenBookMarket(updatedAccountInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'processed',
|
commitment,
|
||||||
[
|
[
|
||||||
{ dataSize: MARKET_STATE_LAYOUT_V2.span },
|
{ dataSize: MARKET_STATE_LAYOUT_V2.span },
|
||||||
{
|
{
|
||||||
memcmp: {
|
memcmp: {
|
||||||
offset: MARKET_STATE_LAYOUT_V2.offsetOf('quoteMint'),
|
offset: MARKET_STATE_LAYOUT_V2.offsetOf('quoteMint'),
|
||||||
bytes: USDC_TOKEN_ID.toBase58(),
|
bytes: quoteToken.mint.toBase58(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
import { PublicKey } from "@solana/web3.js";
|
|
||||||
|
|
||||||
export const USDC_AMOUNT = 0.1; // how much do we spend on each token
|
|
||||||
export const USDC_TOKEN_ID = new PublicKey(
|
|
||||||
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
||||||
);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './constants';
|
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
LiquidityStateV4,
|
LiquidityStateV4,
|
||||||
} from '@raydium-io/raydium-sdk';
|
} from '@raydium-io/raydium-sdk';
|
||||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
import { USDC_TOKEN_ID } from '../common';
|
import { MinimalMarketLayoutV3 } from '../market';
|
||||||
|
|
||||||
export const RAYDIUM_LIQUIDITY_PROGRAM_ID_V4 = MAINNET_PROGRAM_ID.AmmV4;
|
export const RAYDIUM_LIQUIDITY_PROGRAM_ID_V4 = MAINNET_PROGRAM_ID.AmmV4;
|
||||||
export const OPENBOOK_PROGRAM_ID = MAINNET_PROGRAM_ID.OPENBOOK_MARKET;
|
export const OPENBOOK_PROGRAM_ID = MAINNET_PROGRAM_ID.OPENBOOK_MARKET;
|
||||||
@ -31,19 +31,21 @@ export type MinimalLiquidityAccountData = {
|
|||||||
|
|
||||||
export async function getAllAccountsV4(
|
export async function getAllAccountsV4(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
quoteMint: PublicKey,
|
||||||
|
commitment?: Commitment,
|
||||||
): Promise<MinimalLiquidityAccountData[]> {
|
): Promise<MinimalLiquidityAccountData[]> {
|
||||||
const { span } = LIQUIDITY_STATE_LAYOUT_V4;
|
const { span } = LIQUIDITY_STATE_LAYOUT_V4;
|
||||||
const accounts = await connection.getProgramAccounts(
|
const accounts = await connection.getProgramAccounts(
|
||||||
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
||||||
{
|
{
|
||||||
dataSlice: { offset: 0, length: 0 },
|
dataSlice: { offset: 0, length: 0 },
|
||||||
commitment: 'processed',
|
commitment: commitment,
|
||||||
filters: [
|
filters: [
|
||||||
{ dataSize: span },
|
{ dataSize: span },
|
||||||
{
|
{
|
||||||
memcmp: {
|
memcmp: {
|
||||||
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
|
offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
|
||||||
bytes: USDC_TOKEN_ID.toBase58(),
|
bytes: quoteMint.toBase58(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -66,6 +68,46 @@ export async function getAllAccountsV4(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createPoolKeys(
|
||||||
|
id: PublicKey,
|
||||||
|
accountData: LiquidityStateV4,
|
||||||
|
minimalMarketLayoutV3: MinimalMarketLayoutV3,
|
||||||
|
): LiquidityPoolKeys {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
baseMint: accountData.baseMint,
|
||||||
|
quoteMint: accountData.quoteMint,
|
||||||
|
lpMint: accountData.lpMint,
|
||||||
|
baseDecimals: accountData.baseDecimal.toNumber(),
|
||||||
|
quoteDecimals: accountData.quoteDecimal.toNumber(),
|
||||||
|
lpDecimals: 5,
|
||||||
|
version: 4,
|
||||||
|
programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
||||||
|
authority: Liquidity.getAssociatedAuthority({
|
||||||
|
programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4,
|
||||||
|
}).publicKey,
|
||||||
|
openOrders: accountData.openOrders,
|
||||||
|
targetOrders: accountData.targetOrders,
|
||||||
|
baseVault: accountData.baseVault,
|
||||||
|
quoteVault: accountData.quoteVault,
|
||||||
|
marketVersion: 3,
|
||||||
|
marketProgramId: accountData.marketProgramId,
|
||||||
|
marketId: accountData.marketId,
|
||||||
|
marketAuthority: Market.getAssociatedAuthority({
|
||||||
|
programId: accountData.marketProgramId,
|
||||||
|
marketId: accountData.marketId,
|
||||||
|
}).publicKey,
|
||||||
|
marketBaseVault: accountData.baseVault,
|
||||||
|
marketQuoteVault: accountData.quoteVault,
|
||||||
|
marketBids: minimalMarketLayoutV3.bids,
|
||||||
|
marketAsks: minimalMarketLayoutV3.asks,
|
||||||
|
marketEventQueue: minimalMarketLayoutV3.eventQueue,
|
||||||
|
withdrawQueue: accountData.withdrawQueue,
|
||||||
|
lpVault: accountData.lpVault,
|
||||||
|
lookupTableAccount: PublicKey.default,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAccountPoolKeysFromAccountDataV4(
|
export async function getAccountPoolKeysFromAccountDataV4(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
id: PublicKey,
|
id: PublicKey,
|
||||||
@ -73,7 +115,7 @@ export async function getAccountPoolKeysFromAccountDataV4(
|
|||||||
commitment?: Commitment,
|
commitment?: Commitment,
|
||||||
): Promise<LiquidityPoolKeys> {
|
): Promise<LiquidityPoolKeys> {
|
||||||
const marketInfo = await connection.getAccountInfo(accountData.marketId, {
|
const marketInfo = await connection.getAccountInfo(accountData.marketId, {
|
||||||
commitment: commitment ?? 'processed',
|
commitment: commitment,
|
||||||
dataSlice: {
|
dataSlice: {
|
||||||
offset: 253, // eventQueue
|
offset: 253, // eventQueue
|
||||||
length: 32 * 3,
|
length: 32 * 3,
|
||||||
@ -122,10 +164,15 @@ export async function getAccountPoolKeysFromAccountDataV4(
|
|||||||
export async function getTokenAccounts(
|
export async function getTokenAccounts(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
owner: PublicKey,
|
owner: PublicKey,
|
||||||
|
commitment?: Commitment,
|
||||||
) {
|
) {
|
||||||
const tokenResp = await connection.getTokenAccountsByOwner(owner, {
|
const tokenResp = await connection.getTokenAccountsByOwner(
|
||||||
programId: TOKEN_PROGRAM_ID,
|
owner,
|
||||||
});
|
{
|
||||||
|
programId: TOKEN_PROGRAM_ID,
|
||||||
|
},
|
||||||
|
commitment,
|
||||||
|
);
|
||||||
|
|
||||||
const accounts: TokenAccount[] = [];
|
const accounts: TokenAccount[] = [];
|
||||||
for (const { pubkey, account } of tokenResp.value) {
|
for (const { pubkey, account } of tokenResp.value) {
|
||||||
|
|||||||
@ -1,31 +1,36 @@
|
|||||||
import { Connection, PublicKey } from '@solana/web3.js';
|
import { Commitment, Connection, PublicKey } from '@solana/web3.js';
|
||||||
import {
|
import {
|
||||||
|
GetStructureSchema,
|
||||||
MARKET_STATE_LAYOUT_V3,
|
MARKET_STATE_LAYOUT_V3,
|
||||||
} from '@raydium-io/raydium-sdk';
|
} from '@raydium-io/raydium-sdk';
|
||||||
import { USDC_TOKEN_ID } from '../common';
|
|
||||||
import {
|
import {
|
||||||
|
MINIMAL_MARKET_STATE_LAYOUT_V3,
|
||||||
OPENBOOK_PROGRAM_ID,
|
OPENBOOK_PROGRAM_ID,
|
||||||
|
|
||||||
} from '../liquidity';
|
} from '../liquidity';
|
||||||
|
|
||||||
export type MinimalOpenBookAccountData = {
|
export type MinimalOpenBookAccountData = {
|
||||||
id: PublicKey;
|
id: PublicKey;
|
||||||
programId: PublicKey;
|
programId: PublicKey;
|
||||||
};
|
};
|
||||||
|
export type MinimalMarketStateLayoutV3 = typeof MINIMAL_MARKET_STATE_LAYOUT_V3;
|
||||||
|
export type MinimalMarketLayoutV3 =
|
||||||
|
GetStructureSchema<MinimalMarketStateLayoutV3>;
|
||||||
|
|
||||||
export async function getAllMarketsV3(
|
export async function getAllMarketsV3(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
quoteMint: PublicKey,
|
||||||
|
commitment?: Commitment,
|
||||||
): Promise<MinimalOpenBookAccountData[]> {
|
): Promise<MinimalOpenBookAccountData[]> {
|
||||||
const { span } = MARKET_STATE_LAYOUT_V3;
|
const { span } = MARKET_STATE_LAYOUT_V3;
|
||||||
const accounts = await connection.getProgramAccounts(OPENBOOK_PROGRAM_ID, {
|
const accounts = await connection.getProgramAccounts(OPENBOOK_PROGRAM_ID, {
|
||||||
dataSlice: { offset: 0, length: 0 },
|
dataSlice: { offset: 0, length: 0 },
|
||||||
commitment: 'processed',
|
commitment: commitment,
|
||||||
filters: [
|
filters: [
|
||||||
{ dataSize: span },
|
{ dataSize: span },
|
||||||
{
|
{
|
||||||
memcmp: {
|
memcmp: {
|
||||||
offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
|
offset: MARKET_STATE_LAYOUT_V3.offsetOf('quoteMint'),
|
||||||
bytes: USDC_TOKEN_ID.toBase58(),
|
bytes: quoteMint.toBase58(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
BIN
output.png
Normal file
BIN
output.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@ -2,32 +2,6 @@ import { Logger } from "pino";
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the function `fn`
|
|
||||||
* and retries automatically if it fails.
|
|
||||||
*
|
|
||||||
* Tries max `1 + retries` times
|
|
||||||
* with `retryIntervalMs` milliseconds between retries.
|
|
||||||
*
|
|
||||||
* From https://mtsknn.fi/blog/js-retry-on-fail/
|
|
||||||
*/
|
|
||||||
export const retry = async <T>(
|
|
||||||
fn: () => Promise<T> | T,
|
|
||||||
{ retries, retryIntervalMs }: { retries: number; retryIntervalMs: number },
|
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} catch (error) {
|
|
||||||
if (retries <= 0) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
await sleep(retryIntervalMs);
|
|
||||||
return retry(fn, { retries: retries - 1, retryIntervalMs });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
|
export const retrieveEnvVariable = (variableName: string, logger: Logger) => {
|
||||||
const variable = process.env[variableName] || '';
|
const variable = process.env[variableName] || '';
|
||||||
if (!variable) {
|
if (!variable) {
|
||||||
|
|||||||
Reference in New Issue
Block a user