UNPKG

@symmetry-hq/baskets-sdk

Version:

Software Development Kit for interacting with Symmetry Baskets Program

652 lines (625 loc) 26.8 kB
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, ); } }