@symmetry-hq/baskets-v2-sdk
Version:
Symmetry Baskets V2 SDK
378 lines (337 loc) • 13.8 kB
text/typescript
// Core dependencies
import { Program } from "@coral-xyz/anchor";
import { Connection, Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js";
// Local imports
import { BasketsProgram } from "./idl/types";
import { prepareV0Transactions, VersionedTxs } from "./utils/txUtils";
import { updateTokenPricesIxs } from "./instructions/update/updateTokenPrices";
import { withdrawBeforeRebalanceIx } from "./instructions/rebalance/withdrawBeforeRebalance";
import { BasketState, computeRebalanceInfos, fetchBasketState, getBasketTokenPrices, RebalanceInfo } from "./state/basket";
import { depositAfterRebalanceIx } from "./instructions/rebalance/depositAfterRebalance";
import { generateSwapInstruction, getQuoteResponseHandler } from "./instructions/jup";
import { MAX_NUMBER_OF_SWAPS, MAX_SELL_VALUE_PER_TOKEN, MIN_SWAP_VALUE } from "./utils/constants";
import { createAtasIxs } from "./utils/createAtas";
export function getMaxRebalanceAmount(params: {
fromInfo: RebalanceInfo,
toInfo: RebalanceInfo,
}): {
amount: number,
value: number,
} {
const {fromInfo, toInfo} = params;
const valueLimit = Math.min(-fromInfo.valueDiff, toInfo.valueDiff);
const amount = Math.min(
Math.max(fromInfo.maxSpendAmount, 0),
Math.floor(valueLimit * 10 ** fromInfo.tokenDecimals / fromInfo.tokenPrice)
);
const value = amount * fromInfo.tokenPrice / 10 ** fromInfo.tokenDecimals;
return { amount, value };
}
export async function generateRebalanceInstructionsForTokenPair(
sdkParams: {
payer: PublicKey,
connection: Connection,
program: Program<BasketsProgram>,
priorityFee: number,
jupiterApiKey: string,
maxAllowedAccounts: number,
},
params: {
basketState: BasketState,
fromInfo: RebalanceInfo,
toInfo: RebalanceInfo,
slippageBps: number,
minSwapValue: number,
}
): Promise<{
amount: number;
value: number;
ixs: TransactionInstruction[];
luts: PublicKey[];
}> {
const { basketState, fromInfo, toInfo, slippageBps, minSwapValue } = params;
const {amount, value } = getMaxRebalanceAmount({
fromInfo,
toInfo,
});
if (amount === 0 || value < minSwapValue)
return {
amount: 0,
value: 0,
ixs: [],
luts: [],
};
const quoteResponse = await getQuoteResponseHandler({
jupiterApiKey: sdkParams.jupiterApiKey,
maxAllowedAccounts: sdkParams.maxAllowedAccounts,
fromToken: fromInfo.token,
toToken: toInfo.token,
amount: amount,
slippageBps,
});
const withdrawIx = await withdrawBeforeRebalanceIx({
program: sdkParams.program,
basketState: basketState,
payer: sdkParams.payer,
fromTokenMint: fromInfo.token,
toTokenMint: toInfo.token,
amountToWithdraw: amount,
checkWeights: true,
fromTokenWeight: fromInfo.targetWeight,
toTokenWeight: toInfo.targetWeight,
});
const jupIxAndLuts = await generateSwapInstruction({
payer: sdkParams.payer,
jupiterApiKey: sdkParams.jupiterApiKey,
quoteResponse: quoteResponse,
}).catch(() => null);
if (!jupIxAndLuts) {
return {
amount: 0,
value: 0,
ixs: [],
luts: [],
};
}
const depositIx = await depositAfterRebalanceIx({
program: sdkParams.program,
basketState: basketState,
payer: sdkParams.payer,
fromTokenMint: fromInfo.token,
toTokenMint: toInfo.token,
checkWeights: true,
});
return {
amount: amount,
value: value,
ixs: [withdrawIx, jupIxAndLuts.ix, depositIx],
luts: jupIxAndLuts.luts,
};
}
export async function swapTokensHandler(
sdkParams: {
payer: PublicKey,
connection: Connection,
program: Program<BasketsProgram>,
priorityFee: number,
jupiterApiKey: string,
},
params: {
basket: PublicKey;
fromToken: PublicKey;
toToken: PublicKey;
fromAmount: number;
quoteResponse: any;
fromTokenWeight?: number;
toTokenWeight?: number;
}
): Promise<VersionedTxs> {
const basketState: BasketState = await fetchBasketState(sdkParams.program, params.basket);
const fromTokenIndex = basketState.compositionMints.findIndex(mint => mint.toBase58() === params.fromToken.toBase58());
const toTokenIndex = basketState.compositionMints.findIndex(mint => mint.toBase58() === params.toToken.toBase58());
const withdrawIx = await withdrawBeforeRebalanceIx({
program: sdkParams.program,
basketState: basketState,
payer: sdkParams.payer,
fromTokenMint: params.fromToken,
toTokenMint: params.toToken,
amountToWithdraw: params.fromAmount,
checkWeights: false,
fromTokenWeight: params.fromTokenWeight ?? basketState.compositionTargetWeights[fromTokenIndex],
toTokenWeight: params.toTokenWeight ?? basketState.compositionTargetWeights[toTokenIndex],
});
const jupIxAndLuts = await generateSwapInstruction({
payer: sdkParams.payer,
jupiterApiKey: sdkParams.jupiterApiKey,
quoteResponse: params.quoteResponse,
});
const depositIx = await depositAfterRebalanceIx({
program: sdkParams.program,
basketState: basketState,
payer: sdkParams.payer,
fromTokenMint: params.fromToken,
toTokenMint: params.toToken,
checkWeights: false,
});
const preIxs = await createAtasIxs(sdkParams.connection, {
payer: sdkParams.payer,
mints: [params.fromToken, params.toToken],
});
return await prepareV0Transactions({
connection: sdkParams.connection,
payer: sdkParams.payer,
priorityFee: sdkParams.priorityFee,
multipleIxs: [...preIxs, [withdrawIx, jupIxAndLuts.ix, depositIx]],
multipleLookupTableAddresses: [...new Array(preIxs.length).fill([]), jupIxAndLuts.luts],
signers: [...new Array(preIxs.length).fill([]), []],
batches: [preIxs.length, 1],
});
}
export async function rebalanceBasketTokensHandler(
sdkParams: {
payer: PublicKey,
connection: Connection,
program: Program<BasketsProgram>,
priorityFee: number,
jupiterApiKey: string,
maxAllowedAccounts: number,
},
params: {
basket: PublicKey;
fromToken?: PublicKey;
toToken?: PublicKey;
minSwapValue?: number;
maxSellValuePerToken?: number;
maxNumberOfSwaps?: number;
}
): Promise<VersionedTxs> {
// Extract and set default params
const basket = params.basket;
const fromToken = params.fromToken ?? PublicKey.default;
const toToken = params.toToken ?? PublicKey.default;
const minSwapValue = params.minSwapValue ?? MIN_SWAP_VALUE;
const maxSellValue = params.maxSellValuePerToken ?? MAX_SELL_VALUE_PER_TOKEN;
const maxSwaps = params.maxNumberOfSwaps ?? MAX_NUMBER_OF_SWAPS;
// Get price update instructions
const { ixs: updatePricesIxs, luts: updatePricesLuts } = await updateTokenPricesIxs({
program: sdkParams.program,
basket: basket,
});
// Initialize batch arrays
const tokenMints: PublicKey[] = [];
const firstBatchIxs = updatePricesIxs.map(ix => [ix]);
const firstBatchLuts = new Array(updatePricesIxs.length).fill(updatePricesLuts);
const firstBatchSigners = new Array(updatePricesIxs.length).fill([]);
const secondBatchIxs: TransactionInstruction[][] = [];
const secondBatchLuts: PublicKey[][] = [];
const secondBatchSigners: Keypair[][] = [];
// Get basket state and calculate values
const basketState = await fetchBasketState(sdkParams.program, basket);
const slippageBps = basketState.rebalanceSlippageBps;
const oraclePrices = await getBasketTokenPrices(sdkParams.program, basketState);
const { rebalanceInfos } = computeRebalanceInfos({
basketState,
oraclePrices,
});
// Sort rebalance infos by value difference and find relevant tokens
const sortedRebalanceInfos = rebalanceInfos.sort((a, b) => a.valueDiff - b.valueDiff);
let posIndex = 0;
while (posIndex < sortedRebalanceInfos.length && sortedRebalanceInfos[posIndex].valueDiff < 0)
posIndex++;
let negIndex = sortedRebalanceInfos.length - 1;
while (negIndex >= 0 && sortedRebalanceInfos[negIndex].valueDiff > 0)
negIndex--;
for (let i = 0; i < sortedRebalanceInfos.length; i++)
if (sortedRebalanceInfos[i].valueDiff <= 0)
sortedRebalanceInfos[i].valueDiff = Math.max(sortedRebalanceInfos[i].valueDiff, -maxSellValue);
const fromInfo = sortedRebalanceInfos.find(info => info.token.equals(fromToken));
const toInfo = sortedRebalanceInfos.find(info => info.token.equals(toToken));
// Handle direct swap between fromToken and toToken
if (fromInfo && toInfo) {
const { ixs, luts, amount } = await generateRebalanceInstructionsForTokenPair(sdkParams, {
basketState,
fromInfo,
toInfo,
slippageBps,
minSwapValue,
});
if (amount > 0) {
secondBatchIxs.push(ixs);
secondBatchLuts.push(luts);
secondBatchSigners.push([]);
tokenMints.push(fromInfo.token, toInfo.token);
}
}
// Handle selling fromToken to multiple tokens
else if (fromInfo) {
// Iterate through tokens that need value added
for (let i = sortedRebalanceInfos.length - 1; i > negIndex; i--) {
const toInfo = sortedRebalanceInfos[i];
if (fromInfo.valueDiff >= 0 || toInfo.valueDiff <= 0 || toInfo.token.equals(fromToken)) continue;
const { amount, value, ixs, luts } = await generateRebalanceInstructionsForTokenPair(sdkParams, {
basketState,
fromInfo,
toInfo,
slippageBps,
minSwapValue,
});
if (amount === 0) continue;
secondBatchIxs.push(ixs);
secondBatchLuts.push(luts);
secondBatchSigners.push([]);
tokenMints.push(fromInfo.token, toInfo.token);
fromInfo.maxSpendAmount -= amount;
fromInfo.valueDiff += value;
toInfo.valueDiff -= value;
if (secondBatchIxs.length === maxSwaps) break;
}
}
// Handle buying toToken from multiple tokens
else if (toInfo) {
// Iterate through tokens that need value removed
for (let i = 0; i < posIndex; i++) {
const fromInfo = sortedRebalanceInfos[i];
if (fromInfo.valueDiff >= 0 || toInfo.valueDiff <= 0 || toInfo.token.equals(fromToken)) continue;
const { amount, value, ixs, luts } = await generateRebalanceInstructionsForTokenPair(sdkParams, {
basketState,
fromInfo,
toInfo,
slippageBps,
minSwapValue,
});
if (amount === 0) continue;
secondBatchIxs.push(ixs);
secondBatchLuts.push(luts);
secondBatchSigners.push([]);
tokenMints.push(fromInfo.token, toInfo.token);
fromInfo.maxSpendAmount -= amount;
fromInfo.valueDiff += value;
toInfo.valueDiff -= value;
if (secondBatchIxs.length === maxSwaps) break;
}
} else {
for (let i = 0; i < posIndex; i++)
for (let j = sortedRebalanceInfos.length - 1; j > negIndex; j--) {
const fromInfo = sortedRebalanceInfos[i];
const toInfo = sortedRebalanceInfos[j];
if (fromInfo.valueDiff >= 0 || toInfo.valueDiff <= 0 || toInfo.token.equals(fromToken)) continue;
if (fromInfo.token.toBase58() === "8888xhvqnTWZ6tNbfAyP8mLLfaKsqfiW33tgA8RZ8888") continue;
const { amount, value, ixs, luts } = await generateRebalanceInstructionsForTokenPair(sdkParams, {
basketState,
fromInfo,
toInfo,
slippageBps,
minSwapValue,
});
if (amount === 0) continue;
secondBatchIxs.push(ixs);
secondBatchLuts.push(luts);
secondBatchSigners.push([]);
tokenMints.push(fromInfo.token, toInfo.token);
fromInfo.maxSpendAmount -= amount;
fromInfo.valueDiff += value;
toInfo.valueDiff -= value;
if (secondBatchIxs.length === maxSwaps) break;
}
}
if (secondBatchIxs.length === 0)
throw new Error("No swaps to perform");
// Create token accounts if needed
const preIxs = await createAtasIxs(sdkParams.connection, {
payer: sdkParams.payer,
mints: tokenMints,
});
firstBatchIxs.push(...preIxs);
firstBatchLuts.push(...new Array(preIxs.length).fill([]));
firstBatchSigners.push(...new Array(preIxs.length).fill([]));
// Prepare and return transactions
return await prepareV0Transactions({
connection: sdkParams.connection,
payer: sdkParams.payer,
priorityFee: sdkParams.priorityFee,
multipleIxs: [...firstBatchIxs, ...secondBatchIxs],
multipleLookupTableAddresses: [...firstBatchLuts, ...secondBatchLuts],
signers: [...firstBatchSigners, ...secondBatchSigners],
batches: [firstBatchIxs.length, secondBatchIxs.length],
});
}