UNPKG

@symmetry-hq/baskets-v2-sdk

Version:

Symmetry Baskets V2 SDK

421 lines (389 loc) 14.8 kB
// Core dependencies import { AnchorProvider, Program } from "@coral-xyz/anchor"; import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { AccountInfo, AddressLookupTableProgram, Connection, GetProgramAccountsFilter, GetProgramAccountsResponse, Keypair, PublicKey } from "@solana/web3.js"; import { Metaplex } from "@metaplex-foundation/js"; // Local imports import { BASKETS_PROGRAM_ID, DEPOSIT_FEE_SEED, MEME_SOL, MINT_SEED, PYTH_SPONSORED_FEEDS, RAYDIUM_CPMM, RAYDIUM_LIQUIDITY_POOL_V4, REBALANCE_STATE_SEED, STATE_CREATOR_SEED, USDC_DECIMALS, USDC_MINT, WITHDRAW_FEE_SEED, WSOL_DECIMALS, WSOL_MINT } from "./constants"; import { IDL } from "../idl/idl"; import { BasketsProgram } from "../idl/types"; import { parse, v4 } from "uuid"; import { fetchPythSponsoredFeeds } from "../state/oracle"; import { BASKETS_STATE_SIZE, parseBasketState, ParsedBasketState } from "../state/basket"; import { BasketState } from "../state/basket"; import { WithdrawState, WITHDRAW_STATE_SIZE, parseWithdrawState, ParsedWithdrawState } from "../state/withdrawState"; import { PoolInfo } from "../state/oracle"; export function getBasketsProgram( connection: Connection, ): Program<BasketsProgram> { const program: Program<BasketsProgram> = new Program( IDL, new AnchorProvider( connection, new NodeWallet(Keypair.generate()) ) ); return program; } export function getBasketPda( basket: PublicKey, ): PublicKey { return PublicKey.findProgramAddressSync( [Uint8Array.from(basket.toBuffer())], BASKETS_PROGRAM_ID, )[0]; } export function getRebalanceStateAccount(): PublicKey { return PublicKey.findProgramAddressSync( [REBALANCE_STATE_SEED], BASKETS_PROGRAM_ID, )[0]; } export function getStateCreatorAccount(): PublicKey { return PublicKey.findProgramAddressSync( [STATE_CREATOR_SEED], BASKETS_PROGRAM_ID, )[0]; } export function getDepositFeesWallet( basket: PublicKey, ): PublicKey { return PublicKey.findProgramAddressSync( [DEPOSIT_FEE_SEED, basket.toBuffer()], BASKETS_PROGRAM_ID, )[0]; } export function getWithdrawFeesWallet( basket: PublicKey, ): PublicKey { return PublicKey.findProgramAddressSync( [WITHDRAW_FEE_SEED, basket.toBuffer()], BASKETS_PROGRAM_ID, )[0]; } export function getWithdrawStateAccount( withdrawStateSeed: number[], ): PublicKey { return PublicKey.findProgramAddressSync( [Uint8Array.from(withdrawStateSeed)], BASKETS_PROGRAM_ID, )[0]; } export function getBasketTokenMintAccount( basket: PublicKey, ): PublicKey { return PublicKey.findProgramAddressSync( [ MINT_SEED, basket.toBuffer() ], BASKETS_PROGRAM_ID, )[0]; } export function getMetadataAccount( tokenMint: PublicKey ): PublicKey { const metaplex = Metaplex.make(new Connection("https://api.devnet.solana.com")); return metaplex.nfts().pdas().metadata({ mint: tokenMint }); } export function getLookupTableAccount( creator: PublicKey, slot: number, ): PublicKey { const ixAndPubkey = AddressLookupTableProgram.createLookupTable({ authority: creator, recentSlot: slot, payer: creator, }); return ixAndPubkey[1]; } export function getDeactivatedLookupTableAccount( lookupTable: PublicKey, ): PublicKey { return PublicKey.findProgramAddressSync( [lookupTable.toBuffer()], BASKETS_PROGRAM_ID, )[0]; } export function getAta( wallet: PublicKey, tokenMint: PublicKey, ): PublicKey { return getAssociatedTokenAddressSync(tokenMint, wallet, true); } export function getRandomSeed(): number[] { return Array.from(parse(v4())); } export async function getAccountInfos( connection: Connection, keys: PublicKey[], ): Promise<(AccountInfo<Buffer> | null)[]> { const allAccounts: (AccountInfo<Buffer>|null)[] = []; const batchSize = 100; for (let i = 0; i < keys.length; i += batchSize) { const batch = keys.slice(i, i + batchSize); const batchAccounts = await connection.getMultipleAccountsInfo(batch); allAccounts.push(...batchAccounts); } return allAccounts; } async function getRaydiumPools( connection: Connection, tokenMint: PublicKey, programId: PublicKey, dataSize: number, baseTokenOffset: number, quoteTokenOffset: number, baseMintOffset: number, quoteMintOffset: number, ): Promise<PoolInfo[]> { // Get all pools that pair tokenMint with either SOL or USDC const [solQuotePairs, solBasePairs, usdcQuotePairs, usdcBasePairs, memeQuotePairs, memeBasePairs] = await Promise.all([ // Get pools where tokenMint is quote token and SOL is base connection.getProgramAccounts(programId, { filters: [ { dataSize }, { memcmp: { offset: baseMintOffset, bytes: WSOL_MINT.toBase58() } }, { memcmp: { offset: quoteMintOffset, bytes: tokenMint.toBase58() } }, ] }), // Get pools where tokenMint is base token and SOL is quote connection.getProgramAccounts(programId, { filters: [ { dataSize }, { memcmp: { offset: quoteMintOffset, bytes: WSOL_MINT.toBase58() } }, { memcmp: { offset: baseMintOffset, bytes: tokenMint.toBase58() } }, ] }), // Get pools where tokenMint is quote token and USDC is base connection.getProgramAccounts(programId, { filters: [ { dataSize }, { memcmp: { offset: baseMintOffset, bytes: USDC_MINT.toBase58() } }, { memcmp: { offset: quoteMintOffset, bytes: tokenMint.toBase58() } }, ] }), // Get pools where tokenMint is base token and USDC is quote connection.getProgramAccounts(programId, { filters: [ { dataSize }, { memcmp: { offset: quoteMintOffset, bytes: USDC_MINT.toBase58() } }, { memcmp: { offset: baseMintOffset, bytes: tokenMint.toBase58() } }, ] }), // Get pools where tokenMint is quote token and MEME SOL is base connection.getProgramAccounts(programId, { filters: [ { dataSize }, { memcmp: { offset: baseMintOffset, bytes: MEME_SOL.toBase58() } }, { memcmp: { offset: quoteMintOffset, bytes: tokenMint.toBase58() } }, ] }), // Get pools where tokenMint is base token and MEME SOL is quote connection.getProgramAccounts(programId, { filters: [ { dataSize }, { memcmp: { offset: quoteMintOffset, bytes: MEME_SOL.toBase58() } }, { memcmp: { offset: baseMintOffset, bytes: tokenMint.toBase58() } }, ] }), ]); // Combine all pool accounts const allPoolAccounts = [ ...solQuotePairs, ...solBasePairs, ...usdcQuotePairs, ...usdcBasePairs, ...memeQuotePairs, ...memeBasePairs, ]; // Process pool accounts const pools = allPoolAccounts .filter(account => { // For V4 pools, only include active pools (status === 6) if (programId === RAYDIUM_LIQUIDITY_POOL_V4) { const status = parseInt(account.account.data.readBigUInt64LE(0).toString()); return status === 6; } return true; }) .map(account => { // Extract pool data const poolData = { liquidity: 0, poolType: 0, pool: account.pubkey.toBase58(), baseMint: new PublicKey(account.account.data.slice(baseMintOffset, baseMintOffset + 32)).toBase58(), quoteMint: new PublicKey(account.account.data.slice(quoteMintOffset, quoteMintOffset + 32)).toBase58(), baseTokenAccount: new PublicKey(account.account.data.slice(baseTokenOffset, baseTokenOffset + 32)).toBase58(), quoteTokenAccount: new PublicKey(account.account.data.slice(quoteTokenOffset, quoteTokenOffset + 32)).toBase58(), baseBalance: 0, quoteBalance: 0, baseDecimals: 0, quoteDecimals: 0, }; // Ensure tokenMint is always base token if (poolData.baseMint !== tokenMint.toBase58()) { [poolData.quoteMint, poolData.baseMint] = [poolData.baseMint, poolData.quoteMint]; [poolData.quoteTokenAccount, poolData.baseTokenAccount] = [poolData.baseTokenAccount, poolData.quoteTokenAccount]; } return poolData; }); // Collect token accounts for balance lookup const tokenAccountsToLookup = [ ...pools.map(pool => new PublicKey(pool.quoteTokenAccount)), ...pools.map(pool => new PublicKey(pool.baseTokenAccount)), ...pools.map(pool => new PublicKey(pool.quoteMint)), ...pools.map(pool => new PublicKey(pool.baseMint)) ]; // Get account info for all token and mint accounts const accountInfos = await getAccountInfos(connection, tokenAccountsToLookup); // Update pool balances and decimals pools.forEach((pool, i) => { pool.quoteBalance = parseInt(accountInfos[i]?.data.readBigUInt64LE(64).toString() ?? "0"); pool.baseBalance = parseInt(accountInfos[pools.length + i]?.data.readBigUInt64LE(64).toString() ?? "0"); pool.quoteDecimals = accountInfos[2 * pools.length + i]?.data[44] ?? 0; pool.baseDecimals = accountInfos[3 * pools.length + i]?.data[44] ?? 0; }); return pools; } export async function getRaydiumV4Pools( connection: Connection, tokenMint: PublicKey, usdcPrice: number, solPrice: number, ): Promise<PoolInfo[]> { const pools = await getRaydiumPools( connection, tokenMint, RAYDIUM_LIQUIDITY_POOL_V4, 752, 336, 368, 400, 432 ); pools.forEach(pool => { pool.poolType = 2; const price = (pool.quoteMint == WSOL_MINT.toBase58() || pool.quoteMint == MEME_SOL.toBase58()) ? solPrice : usdcPrice; const decimals = (pool.quoteMint == WSOL_MINT.toBase58() || pool.quoteMint == MEME_SOL.toBase58()) ? WSOL_DECIMALS : USDC_DECIMALS; pool.liquidity = price * pool.quoteBalance / 10 ** decimals * 2; }); pools.sort((a, b) => b.liquidity - a.liquidity); return pools.slice(0, 2); } export async function getRaydiumCpmmPools( connection: Connection, tokenMint: PublicKey, usdcPrice: number, solPrice: number, ): Promise<PoolInfo[]> { const pools = await getRaydiumPools( connection, tokenMint, RAYDIUM_CPMM, 637, 72, 104, 168, 200 ); pools.forEach(pool => { pool.poolType = 1; const price = (pool.quoteMint == WSOL_MINT.toBase58() || pool.quoteMint == MEME_SOL.toBase58()) ? solPrice : usdcPrice; const decimals = (pool.quoteMint == WSOL_MINT.toBase58() || pool.quoteMint == MEME_SOL.toBase58()) ? WSOL_DECIMALS : USDC_DECIMALS; pool.liquidity = price * pool.quoteBalance / 10 ** decimals * 2; }); pools.sort((a, b) => b.liquidity - a.liquidity); return pools.slice(0, 2); return pools; } export async function getPythSponsoredFeeds( program: Program<BasketsProgram>, tokenMint: PublicKey, ): Promise<PoolInfo[]> { const pythSponsoredFeeds = await fetchPythSponsoredFeeds(program, PYTH_SPONSORED_FEEDS); const pools = []; for (let i = 0; i < pythSponsoredFeeds.numTokens; i++) if (pythSponsoredFeeds.mints[i].toBase58() == tokenMint.toBase58()) { pools.push({ liquidity: 999999999, poolType: 0, pool: pythSponsoredFeeds.ownAddress.toBase58(), baseMint: pythSponsoredFeeds.mints[i].toBase58(), quoteMint: USDC_MINT.toBase58(), baseTokenAccount: pythSponsoredFeeds.feeds[i].toBase58(), quoteTokenAccount: PublicKey.default.toBase58(), baseBalance: 0, quoteBalance: 0, baseDecimals: (await getAccountInfos(program.provider.connection, [tokenMint]))[0]?.data[44] ?? 0, quoteDecimals: 6, }); } return pools; } export async function getAllBaskets( program: Program<BasketsProgram>, ): Promise<ParsedBasketState[]> { const accounts: BasketState[] = (await program.account.basketV200.all()).map(account => account.account); return accounts.map(account => parseBasketState(account)); } export async function getBasketsByCreator( program: Program<BasketsProgram>, creator: PublicKey, ): Promise<ParsedBasketState[]> { const accountFilters: GetProgramAccountsFilter[] = [ { dataSize: BASKETS_STATE_SIZE + 8, }, { memcmp: { offset: 8 + 1 + 32 + 1 + 32 + 32 + 8 + 8 + 8 + 8, bytes: creator.toBase58(), }, } ] const accounts: GetProgramAccountsResponse = await program.provider.connection .getProgramAccounts( BASKETS_PROGRAM_ID, { commitment: "confirmed", filters: accountFilters, encoding: 'base64' } ); const baskets: BasketState[] = accounts.map(account => program.coder.accounts.decode("basketV200", account.account.data) ); return await Promise.all(baskets.map(basket => parseBasketState(basket))); } export async function getWithdrawStatesByUser( program: Program<BasketsProgram>, user: PublicKey, ): Promise<ParsedWithdrawState[]> { const accountFilters: GetProgramAccountsFilter[] = [ { dataSize: WITHDRAW_STATE_SIZE + 8, }, { memcmp: { offset: 8 + 32 + 16 + 32 , bytes: user.toBase58(), }, } ] const accounts: GetProgramAccountsResponse = await program.provider.connection .getProgramAccounts( BASKETS_PROGRAM_ID, { commitment: "confirmed", filters: accountFilters, encoding: 'base64' } ); const withdrawStates: WithdrawState[] = accounts.map(account => program.coder.accounts.decode("withdrawStateV200", account.account.data) ); return withdrawStates.map(withdrawState => parseWithdrawState(withdrawState)); }