@symmetry-hq/baskets-v2-sdk
Version:
Symmetry Baskets V2 SDK
421 lines (389 loc) • 14.8 kB
text/typescript
// 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));
}