@symmetry-hq/baskets-sdk
Version:
Software Development Kit for interacting with Symmetry Baskets Program
652 lines (625 loc) • 26.8 kB
text/typescript
import { BN, Program, Wallet } from "@coral-xyz/anchor";
import {
AccountInfo,
AddressLookupTableAccount,
ComputeBudgetProgram,
Connection,
GetProgramAccountsResponse,
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
TransactionMessage,
TransactionSignature,
VersionedTransaction
} from "@solana/web3.js";
import { BasketsIDL } from "./basketsIDL";
import {
BuyStateChainData,
BASKETS_PROGRAM_PDA,
RebalanceInfo,
REBALANCE_FEE_ACCOUNT,
Side, BUY_FEE_ACCOUNT,
ADDITIONAL_FEE,
ADDITIONAL_UNITS,
TokenSettings,
TOKEN_LIST_ADDRESS,
JupSwapData,
JUP_AGGREGATOR,
BUY_FEE_WALLET,
BEYOND_LST_BASKET,
TransactionToSend,
} from "./config";
import {
sendSignedTransactions,
signTransactionsWithWallet,
signVersionedTransactions,
} from "./utils";
import { Basket } from "./basketState";
import { AccountLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createSyncNativeInstruction, getAssociatedTokenAddressSync } from "./splTokenHelpers";
import { buildBuyBasketIx, buildBuyBasketWithMultipleTokensIx, buildBuyBasketWithSingleTokenIx, buildClaimTokensFromBuyStateIxs, buildMintFromBuyStateIx, updateOraclesTxs } from "./instructionsBuilder";
export class BuyState {
public ownAddress: PublicKey;
public data: BuyStateChainData;
public basket: Basket;
constructor(
ownAddress: PublicKey,
buyStateData: BuyStateChainData,
basket: Basket,
) {
this.ownAddress = ownAddress;
this.data = buyStateData;
this.basket = basket;
}
static async loadFromRawData(
program: Program<BasketsIDL>,
rawData: {
pubkey: PublicKey;
account: AccountInfo<Buffer>;
},
basket?: Basket,
): Promise<BuyState> {
let buyStateData = program.coder.accounts.decode("buyState", rawData.account.data);
if (!basket)
basket = await Basket.loadFromPubkey(program, buyStateData.fund);
return new BuyState(
rawData.pubkey,
buyStateData,
basket
)
}
static async loadMultiple(
program: Program<BasketsIDL>,
rawDatas: GetProgramAccountsResponse,
): Promise<BuyState[]> {
let buyStateDatas = [];
let baskets: PublicKey[] = [];
for (let i = 0; i < rawDatas.length; i++) {
let decoded = program.coder.accounts
.decode("buyState", rawDatas[i].account.data);
buyStateDatas.push(
rawDatas[i]
);
baskets.push(decoded.fund);
}
let basketsData = await program.provider.connection
.getMultipleAccountsInfo(baskets, "confirmed");
let buyStates: BuyState[] = [];
for (let i = 0; i < buyStateDatas.length; i++) {
let basket = Basket.loadFromRawData(
program,
{
pubkey: baskets[i],
//@ts-ignore
account: basketsData[i],
}
);
buyStates.push(await this.loadFromRawData(
program,
buyStateDatas[i],
basket
));
}
return buyStates;
}
static async loadFromPubkey(
program: Program<BasketsIDL>,
buyState: PublicKey,
basket?: Basket,
): Promise<BuyState> {
let buyStateData = await program.account.buyState.fetch(buyState, "confirmed");
if (!basket)
basket = await Basket.loadFromPubkey(program, buyStateData.fund);
return new BuyState(
buyState,
//@ts-ignore
buyStateData,
basket,
);
}
static computeMintAmountWithMultipleTokens(
tokenList: TokenSettings[],
basket: Basket,
contribution: {token: PublicKey, amount: number}[],
oraclePrices: number[],
): number {
let contributions = [];
let totalContribution = 0;
let basketWorth = 0;
for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++) {
let tokenPrice = oraclePrices[basket.data.currentCompToken[i].toNumber()];
let amount = 0;
for (let j = 0; j < contribution.length; j++)
if (contribution[j].token.toBase58() == tokenList[basket.data.currentCompToken[i].toNumber()].tokenMint)
amount = contribution[j].amount;
totalContribution += tokenPrice * amount;
contributions.push(tokenPrice * amount);
basketWorth += tokenPrice
* parseInt(basket.data.currentCompAmount[i].toString())
/ 10 ** tokenList[basket.data.currentCompToken[i].toNumber()].decimals;
}
let valueToRebalance = 0;
for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++) {
let recommendedContribution = totalContribution
* basket.data.targetWeight[i].toNumber()
/ basket.data.weightSum.toNumber();
if (recommendedContribution > contributions[i])
valueToRebalance += recommendedContribution + contributions[i]; else
valueToRebalance += contributions[i] - recommendedContribution;
}
totalContribution -= valueToRebalance * basket.data.rebalanceSlippage.toNumber() / 10000;
if (basket.data.supplyOutstanding.toNumber() == 0)
return totalContribution / 100; else
return totalContribution
* basket.data.supplyOutstanding.toNumber()
/ 10 ** 6
/ basketWorth;
}
// static async multipleTokensDeposit(
// program: Program<BasketsIDL>,
// wallet: Wallet,
// tokenList: TokenSettings[],
// basket: Basket,
// contribution: {token: PublicKey, amount: number}[],
// lamports: number,
// updateOracles: boolean, // NEDD TO IMPLEMENT
// ): Promise <TransactionSignature> {
// let connection: Connection = program.provider.connection;
// let tokenMints = basket.data.currentCompToken.slice(0, basket.data.numOfTokens.toNumber())
// .map(token => new PublicKey(tokenList[token.toNumber()].tokenMint));
// tokenMints.push(basket.data.fundToken);
// let buyerAtas = tokenMints.map(token => getAssociatedTokenAddressSync(
// token,
// wallet.publicKey,
// true,
// ));
// let infobuyerAtas = await connection.getMultipleAccountsInfo(buyerAtas, "confirmed");
// let preTransaction = new Transaction();
// preTransaction.instructions = infobuyerAtas
// .map((info, id) => { return {id: id, info: info} })
// .filter(x => x.info == null).map(x =>
// createAssociatedTokenAccountInstruction(
// wallet.publicKey,
// buyerAtas[x.id],
// wallet.publicKey,
// tokenMints[x.id],
// )
// );
// let wSolIndex = tokenMints.findIndex(mint => mint.toBase58() == NATIVE_MINT.toBase58());
// if (wSolIndex != -1) {
// //@ts-ignore
// let info: AccountInfo<Buffer> = infobuyerAtas[wSolIndex];
// let amount = 0;
// for (let i = 0; i < contribution.length; i++) {
// if (contribution[i].token.toBase58() == NATIVE_MINT.toBase58())
// amount = contribution[i].amount;
// }
// let toDeposit = Math.floor(amount * 10**9);
// if (info) {
// let parsedInfo = AccountLayout.decode(info.data);
// toDeposit -= parseInt(parsedInfo.amount.toString());
// }
// if (toDeposit > 0) {
// preTransaction.add(
// SystemProgram.transfer({
// fromPubkey: wallet.publicKey,
// toPubkey: buyerAtas[wSolIndex],
// lamports: toDeposit
// }),
// ).add(
// createSyncNativeInstruction(buyerAtas[wSolIndex], TOKEN_PROGRAM_ID)
// );
// }
// }
// let transaction = new Transaction();
// transaction.instructions = [
// await buildBuyBasketWithMultipleTokensIx(program, tokenList, wallet.publicKey, basket, contribution),
// ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
// ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
// ];
// let signedTransactions = await signTransactionsWithWallet(
// connection,
// wallet,
// [
// {transaction: preTransaction, signers: []},
// {transaction: transaction, signers: []}
// ]
// );
// let txs = await sendSignedTransactions(
// connection,
// signedTransactions,
// 2
// );
// return txs[1];
// }
static computeMintAmountWithSingleToken(
tokenList: TokenSettings[],
basket: Basket,
tokenSettings: TokenSettings,
amount: number,
oraclePrices: number[],
): number {
let tokenWorth = [];
let basketWorth = 0;
for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++) {
let tokenPrice = oraclePrices[basket.data.currentCompToken[i].toNumber()];
let usdValue = tokenPrice
* parseInt(basket.data.currentCompAmount[i].toString())
/ 10 ** tokenList[basket.data.currentCompToken[i].toNumber()].decimals;
tokenWorth.push(usdValue);
basketWorth += usdValue;
}
let totalFee = 10;
totalFee += basket.data.hostFee.toNumber();
totalFee += basket.data.managerFee.toNumber();
if (basket.ownAddress.toBase58() == BEYOND_LST_BASKET.toBase58()) totalFee = 0;
amount = amount * (10000 - totalFee) / 10000;
let totalContribution = amount * oraclePrices[tokenSettings.id];
let valueToRebalance = 0;
for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++) {
let valueBefore = tokenWorth[i];
let valueAfter = tokenWorth[i];
if (basket.data.currentCompToken[i].toNumber() == tokenSettings.id)
valueAfter += totalContribution;
let targetValueBefore = basketWorth *
basket.data.targetWeight[i].toNumber() /
basket.data.weightSum.toNumber();
let targetValueAfter = (basketWorth + totalContribution) *
basket.data.targetWeight[i].toNumber() /
basket.data.weightSum.toNumber();
if (basket.data.currentCompToken[i].toNumber() != tokenSettings.id) {
valueToRebalance += Math.max(targetValueAfter, valueAfter) -
Math.max(targetValueBefore, valueAfter)
} else {
if (valueAfter <= targetValueAfter) { continue; }
let overflow = valueAfter - targetValueAfter;
if (valueBefore <= targetValueBefore)
valueToRebalance += overflow;
else
valueToRebalance += overflow - (valueBefore - targetValueBefore);
}
}
if (basket.ownAddress.toBase58() == BEYOND_LST_BASKET.toBase58()) valueToRebalance = 0;
totalContribution -= valueToRebalance * 300 / 10000;
if (basket.data.supplyOutstanding.toNumber() == 0)
return totalContribution / 100; else
return totalContribution
* basket.data.supplyOutstanding.toNumber()
/ 10 ** 6
/ basketWorth;
}
static async singleTokenDeposit(
program: Program<BasketsIDL>,
wallet: Wallet,
tokenList: TokenSettings[],
basket: Basket,
tokenMint: PublicKey,
amount: number,
lamports: number,
updateOracles: boolean, /// NEED To Implement
): Promise <TransactionSignature> {
let connection: Connection = program.provider.connection;
let buyerTokenAccount = getAssociatedTokenAddressSync(tokenMint, wallet.publicKey, true);
let buyerBasketTokenAccount = getAssociatedTokenAddressSync(basket.data.fundToken, wallet.publicKey, true);
let symmetryFeeAccount = getAssociatedTokenAddressSync(tokenMint, BUY_FEE_WALLET);
let hostFeeAccount = getAssociatedTokenAddressSync(tokenMint, basket.data.hostPubkey, true);
let managerFeeAccount = getAssociatedTokenAddressSync(
tokenMint,
basket.data.feeDelegate.toBase58() == PublicKey.default.toBase58()
? basket.data.manager : basket.data.feeDelegate,
true
);
let infobuyerAtas = await connection.getMultipleAccountsInfo(
[buyerTokenAccount, buyerBasketTokenAccount, symmetryFeeAccount, hostFeeAccount, managerFeeAccount],
"confirmed"
);
let preTransaction = new Transaction();
if (!infobuyerAtas[0])
preTransaction.add(createAssociatedTokenAccountInstruction(
wallet.publicKey,
buyerTokenAccount,
wallet.publicKey,
tokenMint
))
if (!infobuyerAtas[1])
preTransaction.add(createAssociatedTokenAccountInstruction(
wallet.publicKey,
buyerBasketTokenAccount,
wallet.publicKey,
basket.data.fundToken
))
if (!infobuyerAtas[2])
preTransaction.add(createAssociatedTokenAccountInstruction(
wallet.publicKey,
symmetryFeeAccount,
BUY_FEE_WALLET,
tokenMint
))
if (tokenMint.toBase58() == NATIVE_MINT.toBase58()) {
//@ts-ignore
let info: AccountInfo<Buffer> = infobuyerAtas[0];
let toDeposit = Math.floor(amount * 10**9);
if (info) {
let parsedInfo = AccountLayout.decode(info.data);
toDeposit -= parseInt(parsedInfo.amount.toString());
}
if (toDeposit > 0) {
preTransaction.add(
SystemProgram.transfer({
fromPubkey: wallet.publicKey,
toPubkey: buyerTokenAccount,
lamports: toDeposit
}),
).add(
createSyncNativeInstruction(buyerTokenAccount, TOKEN_PROGRAM_ID)
);
}
}
preTransaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}));
preTransaction.add(ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports}));
let mainIx = await buildBuyBasketWithSingleTokenIx(program, tokenList, wallet.publicKey, basket, tokenMint, amount);
let transaction = new Transaction();
transaction.instructions = [
mainIx,
ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
];
let signedTransactions = await signTransactionsWithWallet(
connection,
wallet,
[
{transaction: preTransaction, signers: []},
{transaction: transaction, signers: []}
]
);
let txs = await sendSignedTransactions(
connection,
signedTransactions,
2
);
return txs[1];
}
static async createNew(
program: Program<BasketsIDL>,
wallet: Wallet,
tokenList: TokenSettings[],
basket: Basket,
amount: number,
lamports: number = ADDITIONAL_FEE,
): Promise<BuyState> {
let connection: Connection = program.provider.connection;
let buyData = await buildBuyBasketIx(program, tokenList, wallet.publicKey, basket, amount);
let transaction = new Transaction();
transaction.instructions = [
buyData,
ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
];
let signedTransactions = await signTransactionsWithWallet(
connection,
wallet,
[{transaction: transaction, signers: []}]
);
await sendSignedTransactions(
connection,
signedTransactions,
1
);
return await BuyState.loadFromPubkey(program, buyData.keys[11].pubkey, basket);
}
async update(program: Program<BasketsIDL>): Promise<void> {
//@ts-ignore
this.data = await program.account.buyState.fetch(this.ownAddress, "confirmed");
}
getBuyStateRebalanceInfo(
tokenList: TokenSettings[],
): RebalanceInfo[] {
let currentTokens = Array.from(this.data.token, x => x.toNumber());
let amountToSpend = Array.from(this.data.amountToSpend, x => parseInt(x.toString()));
let rebalanceInfos: RebalanceInfo[] = [];
for (let i = 1; i < currentTokens.length; i++)
if (amountToSpend[i] > 0)
rebalanceInfos.push({
tokenId: currentTokens[i],
tokenAccountFrom: tokenList[0].pdaTokenAccount,
mintFrom: tokenList[0].tokenMint,
oracleFrom: tokenList[0].oracleAccount,
tokenAccountTo: tokenList[currentTokens[i]].pdaTokenAccount,
mintTo: tokenList[currentTokens[i]].tokenMint,
oracleTo: tokenList[currentTokens[i]].oracleAccount,
amountFrom: amountToSpend[i],
decimals: tokenList[0].decimals,
volume: amountToSpend[i] / 10 * tokenList[0].decimals,
side: Side.From
})
return rebalanceInfos.filter(x => x.volume > 0.005);
}
async rebalanceBuyState(
program: Program<BasketsIDL>,
wallet: Wallet,
tokenList: TokenSettings[],
jupSwapDatas: JupSwapData[],
lamports: number,
updateOraclesTxData: TransactionToSend[],
lookups: AddressLookupTableAccount[],
): Promise<TransactionSignature[]> {
let basket = this.basket;
let transactionsData: TransactionToSend[] = updateOraclesTxData;
for (let i = 0; i < jupSwapDatas.length; i++) {
let rebalanceData = jupSwapDatas[i];
if (!rebalanceData)
continue;
let tokenId = rebalanceData.toTokenId;
let ix = (rebalanceData.type == "Simple") ?
await program.methods
.rebalanceBuyState(
tokenId,
new BN(rebalanceData.fromAmount),
rebalanceData.dataLength,
Array.from(rebalanceData.data),
)
.accounts({
fundState: basket.ownAddress,
buyState: this.ownAddress,
tokenList: TOKEN_LIST_ADDRESS,
oracleSol: new PublicKey(tokenList[1].oracleAccount),
oracleToken: new PublicKey(tokenList[tokenId].oracleAccount),
oracleUsdc: new PublicKey(tokenList[0].oracleAccount),
pdaAccount: BASKETS_PROGRAM_PDA,
pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
pdaUsdcAccount: new PublicKey(tokenList[0].pdaTokenAccount),
rebalanceFeeAccount: REBALANCE_FEE_ACCOUNT,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(rebalanceData.accounts)
.instruction() :
await program.methods
.rebalanceBuyStateTransitive(
tokenId,
new BN(rebalanceData.fromAmount),
rebalanceData.firstIxEnd,
rebalanceData.dataLength,
rebalanceData.firstIxAccounts,
Array.from(rebalanceData.data),
)
.accounts({
fundState: basket.ownAddress,
buyState: this.ownAddress,
tokenList: TOKEN_LIST_ADDRESS,
oracleSol: new PublicKey(tokenList[1].oracleAccount),
oracleToken: new PublicKey(tokenList[tokenId].oracleAccount),
oracleUsdc: new PublicKey(tokenList[0].oracleAccount),
pdaAccount: BASKETS_PROGRAM_PDA,
pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
pdaMidAccount: new PublicKey(rebalanceData.midTokenPda),
pdaUsdcAccount: new PublicKey(tokenList[0].pdaTokenAccount),
rebalanceFeeAccount: REBALANCE_FEE_ACCOUNT,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(rebalanceData.accounts)
.instruction()
transactionsData.push({
payerKey: wallet.publicKey,
instructions: [
ix,
ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
],
lookupTables: [...lookups, ...rebalanceData.lookupTableAccounts]
});
}
const blockhash = (await program.provider.connection.getLatestBlockhash("confirmed")).blockhash;
let signedTransactions = await signVersionedTransactions(
wallet,
transactionsData.map(tx => new VersionedTransaction(
new TransactionMessage({
payerKey: tx.payerKey,
recentBlockhash: blockhash,
instructions: tx.instructions,
}).compileToV0Message(tx.lookupTables)
))
);
return await sendSignedTransactions(
program.provider.connection,
signedTransactions,
updateOraclesTxData.length > 0 ? 1 : 0,
);
}
async mint(
program: Program<BasketsIDL>,
swbProgram: Program,
wallet: Wallet,
tokenList: TokenSettings[],
lookups: AddressLookupTableAccount[],
lamports: number,
updateOracles: boolean,
): Promise<TransactionSignature[]> {
let transactionsData: TransactionToSend[] = [];
if (updateOracles)
transactionsData = await updateOraclesTxs(
swbProgram,
wallet.publicKey,
this.basket.getSwbFeeds(tokenList),
lamports,
);
transactionsData.map(tx => tx.lookupTables = [...lookups, ...tx.lookupTables])
transactionsData.push({
payerKey: wallet.publicKey,
instructions: [
await buildMintFromBuyStateIx(program, tokenList, wallet.publicKey, this.basket, this),
ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
],
lookupTables: lookups
});
const blockhash = (await program.provider.connection.getLatestBlockhash("confirmed")).blockhash;
let signedTransactions = await signVersionedTransactions(
wallet,
transactionsData.map(tx => new VersionedTransaction(
new TransactionMessage({
payerKey: tx.payerKey,
recentBlockhash: blockhash,
instructions: tx.instructions,
}).compileToV0Message(tx.lookupTables)
))
);
return await sendSignedTransactions(
program.provider.connection,
signedTransactions,
signedTransactions.length,
);
}
async claimTokens(
program: Program<BasketsIDL>,
wallet: Wallet,
tokenList: TokenSettings[],
lamports: number = ADDITIONAL_FEE,
): Promise<TransactionSignature[]> {
let buyer = this.data.buyer;
let connection = program.provider.connection;
let transactions: Transaction[] = [];
let ixId = 0;
let claimIxs = await buildClaimTokensFromBuyStateIxs(program, tokenList, wallet.publicKey, this.basket, this);
for (let i = 0; i < this.data.token.length; i++) {
if (parseInt(this.data.amountBought[i].toString()) == 0 && i != 0)
continue;
let transaction = new Transaction();
let tokenId = this.data.token[i].toNumber();
let userTokenAccount = getAssociatedTokenAddressSync(
new PublicKey(tokenList[tokenId].tokenMint),
buyer,
true,
);
let infoAta = await connection.getAccountInfo(userTokenAccount, "confirmed");
if (!infoAta)
transaction.add(
await createAssociatedTokenAccountInstruction(
wallet.publicKey,
userTokenAccount,
buyer,
new PublicKey(tokenList[tokenId].tokenMint),
)
);
transaction.instructions = [
...transaction.instructions,
claimIxs[ixId],
ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
]
transactions.push(transaction);
ixId = ixId + 1;
}
let signedTransactions = await signTransactionsWithWallet(
connection,
wallet,
transactions.map(transaction => {
return { transaction: transaction, signers: []}
})
);
return await sendSignedTransactions(
connection,
signedTransactions,
);
}
}