|
@@ -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;
|
|
|
+async function buy(
|
|
|
+ accountId: PublicKey,
|
|
|
+ accountData: LiquidityStateV4,
|
|
|
+): Promise<void> {
|
|
|
+ const tokenAccount = existingTokenAccounts.get(
|
|
|
+ accountData.baseMint.toString(),
|
|
|
+ );
|
|
|
|
|
|
- if (!tokenAccountOut) {
|
|
|
- logger.info(`No token account for ${baseMint}`);
|
|
|
+ if (!tokenAccount) {
|
|
|
return;
|
|
|
}
|
|
|
+
|
|
|
+ tokenAccount.poolKeys = createPoolKeys(
|
|
|
+ accountId,
|
|
|
+ accountData,
|
|
|
+ tokenAccount.market!,
|
|
|
+ );
|
|
|
const { innerTransaction, address } = Liquidity.makeSwapFixedInInstruction(
|
|
|
{
|
|
|
- poolKeys,
|
|
|
+ poolKeys: tokenAccount.poolKeys,
|
|
|
userKeys: {
|
|
|
- tokenAccountIn: usdcTokenKey,
|
|
|
- tokenAccountOut: tokenAccountOut,
|
|
|
+ tokenAccountIn: quoteTokenAssociatedAddress,
|
|
|
+ tokenAccountOut: tokenAccount.address,
|
|
|
owner: wallet.publicKey,
|
|
|
},
|
|
|
- amountIn: USDC_AMOUNT * 1000000,
|
|
|
+ amountIn: quoteAmount.raw,
|
|
|
minAmountOut: 0,
|
|
|
},
|
|
|
- poolKeys.version,
|
|
|
+ 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(),
|
|
|
},
|
|
|
},
|
|
|
],
|