UNPKG

@symmetry-hq/baskets-sdk

Version:

Software Development Kit for interacting with Symmetry Baskets Program

966 lines (902 loc) 38.9 kB
import { AnchorProvider, BN, Program, Wallet } from "@coral-xyz/anchor"; import { AccountInfo, AddressLookupTableAccount, AddressLookupTableState, Connection, GetProgramAccountsFilter, GetProgramAccountsResponse, Keypair, MessageV0, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionSignature, VersionedTransaction } from "@solana/web3.js" import { BasketsIDL, IDL } from "./basketsIDL"; import { BPS_DIVIDER, COMBINED_TOKENS_IN_A_BASKET, CreateBasketParams, FilterType, BasketError, BASKETS_PROGRAM_ID, BASKETS_PROGRAM_PDA, RebalanceInfo, Rule, Side, TokenSettings, TOKEN_LIST_ADDRESS, TOKEN_STATS_ADDRESS, WeightType, REBALANCE_FEE_ACCOUNT, JupSwapData} from "./config"; import { parsePriceData } from '@pythnetwork/client'; import axios from "axios"; import { Basket } from "./basketState"; import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; import { AccountLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createSyncNativeInstruction, getAssociatedTokenAddressSync } from "./splTokenHelpers"; export function delay(ms: number) { return new Promise( resolve => setTimeout(resolve, ms) ); } export async function rawOraclePrices( connection: Connection, tokenList: any, ): Promise<number[]> { let oraclePricesData = await connection .getMultipleAccountsInfo(tokenList.map((x: any) => x.oracleAccount), "confirmed"); let oraclePrices = []; for(let i=0; i<oraclePricesData.length; i++){ if(oraclePricesData[i] == null) { oraclePrices.push(0); continue; } if (tokenList[i].oracleType == 0) //@ts-ignore oraclePrices.push(parsePriceData(oraclePricesData[i].data).aggregate.price); else if (tokenList[i].oracleType == 1) { let price = parseInt(new BN(//@ts-ignore oraclePricesData[i].data.subarray( tokenList[i].oracleIndex * 8 + 9, tokenList[i].oracleIndex * 8 + 17 ), 10, "le" ).toString()) / 10 ** 12; price = price * (10000 - tokenList[i].oracleConfidencePct) / 10000; oraclePrices.push(price); } else if (tokenList[i].oracleType == 2) { let total_lamports = parseInt(new BN(//@ts-ignore oraclePricesData[i].data.subarray( 1+3*32+1+5*32, 1+3*32+1+5*32+8 ), 10, "le" ).toString()); let pool_token_supply = parseInt(new BN(//@ts-ignore oraclePricesData.data.subarray( 1+3*32+1+5*32+8, 1+3*32+1+5*32+16 ), 10, "le" ).toString()); oraclePrices.push(total_lamports / pool_token_supply) } else if (tokenList[i].oracleType == 3) { //@ts-ignore let data: Buffer = oraclePricesData[i].data; let price = data.readBigInt64LE(73); let exp = data.readInt32LE(89); oraclePrices.push(Number(price) * 10 ** exp); } else if (tokenList[i].oracleType == 4) { //@ts-ignore let priceObj = oraclePricesData[i].data.subarray(2264, 2392); let price = parseInt(new BN(priceObj.subarray(32, 48), 10, "le").toString()); oraclePrices.push(price / 10 ** 18); } else { oraclePrices.push(0); } } for (let i = 0; i < oraclePrices.length; i++) if (tokenList[i].oracleType == 2) oraclePrices[i] *= oraclePrices[1]; return oraclePrices; } export async function getOraclePrices( program: Program<BasketsIDL>, tokenList: TokenSettings[] ): Promise<number[]> { let oraclePricesData = ( await program.provider.connection .getMultipleAccountsInfo( tokenList.map(x => new PublicKey(x.oracleAccount)), "confirmed" ) ); let oraclePrices = []; for(let i=0; i<oraclePricesData.length; i++){ if(oraclePricesData[i] == null){ oraclePrices.push(0); } else { if (tokenList[i].oracleType == "Pyth") //@ts-ignore oraclePrices.push(parsePriceData(oraclePricesData[i].data).aggregate.price); else if (tokenList[i].oracleType == "Switchboard") { let price = parseInt(new BN(//@ts-ignore oraclePricesData[i].data.subarray( tokenList[i].oracleIndex * 8 + 9, tokenList[i].oracleIndex * 8 + 17 ), 10, "le" ).toString()) / 10 ** 12; price = price * (10000 - tokenList[i].oracleConfidencePct) / 10000; oraclePrices.push(price); } else if (tokenList[i].oracleType == "LST") { let total_lamports = parseInt(new BN( //@ts-ignore oraclePricesData[i].data.subarray( 1+3*32+1+5*32, 1+3*32+1+5*32+8 ), 10, "le" ).toString()); let pool_token_supply = parseInt(new BN(//@ts-ignore oraclePricesData[i].data.subarray( 1+3*32+1+5*32+8, 1+3*32+1+5*32+16 ), 10, "le" ).toString()); oraclePrices.push(total_lamports / pool_token_supply); } else if (tokenList[i].oracleType == "PythSponsored") { //@ts-ignore let data: Buffer = oraclePricesData[i].data; let price = data.readBigInt64LE(73); let exp = data.readInt32LE(89); oraclePrices.push(Number(price) * 10 ** exp); } else if (tokenList[i].oracleType == "SwbOnDemand") { //@ts-ignore let priceObj = oraclePricesData[i].data.subarray(2264, 2392); let price = parseInt(new BN(priceObj.subarray(32, 48), 10, "le").toString()); oraclePrices.push(price / 10 ** 18); } else { oraclePrices.push(0); } } } for (let i = 0; i < oraclePrices.length; i++) if (tokenList[i].oracleType == "LST") oraclePrices[i] *= oraclePrices[1]; return oraclePrices; } export async function getFilteredProgramAccounts( connection: Connection, filters: GetProgramAccountsFilter[] ): Promise<GetProgramAccountsResponse> { return await connection .getProgramAccounts( BASKETS_PROGRAM_ID, { commitment: "confirmed", filters, encoding: 'base64' } ) } export async function signVersionedTransactions( wallet: Wallet, transactions: VersionedTransaction[], ): Promise<VersionedTransaction[]> { let txs: VersionedTransaction[] = []; // @ts-ignore txs = await wallet.signAllTransactions(transactions).catch((e) => { console.log("Couldn't sign transactions: " + e.message); return null; }); if (!txs) { try { // @ts-ignore transactions.map(tx => tx.sign([wallet.payer])) txs = transactions; } catch (e: any) { console.log("Couldn't sign transactions: " + e.message); } } return txs; } export async function signTransactionsWithWallet( connection: Connection, wallet: Wallet, transactionsData: { transaction: Transaction, signers: Keypair[], }[] ): Promise<Transaction[]> { if (transactionsData.length == 0) return []; let { blockhash } = await connection.getLatestBlockhash("confirmed"); for (let i = 0; i < transactionsData.length; i++) { transactionsData[i].transaction.feePayer = wallet.publicKey; transactionsData[i].transaction.recentBlockhash = blockhash; if (transactionsData[i].signers.length > 0) transactionsData[i].transaction.partialSign( ...transactionsData[i].signers ); } return await wallet.signAllTransactions( transactionsData.map(data => data.transaction) ).catch((e) => { throw new BasketError("Couldn't sign transactions: " + e.message)}); } export async function sendSignedTransaction( connection: Connection, transaction: Transaction|VersionedTransaction, retries: number = 2, delayMs: number = 400, ): Promise<TransactionSignature> { let serialized = transaction.serialize(); let txId = await connection.sendRawTransaction( serialized, {skipPreflight: true, preflightCommitment: "processed", maxRetries: 3}, ).catch((e) => { throw new BasketError("Couldn't send transaction: " + e.message) }); connection.sendRawTransaction( serialized, {skipPreflight: false, preflightCommitment: "processed", maxRetries: 3}, ).catch((e) => console.log(txId + " : " + e.message)); for (let numDelay = 1; numDelay < retries; numDelay++) delay(delayMs * numDelay).then(() => { connection. sendRawTransaction(serialized, {skipPreflight: true}).catch(() => {}); }) return txId; } export async function confirmTransaction( connection: Connection, txId: TransactionSignature, timeout: number = 30, ): Promise<boolean> { let result = undefined; for (let _ = 0; _ < timeout && result == undefined; _++) { await delay(1000); await connection .getTransaction(txId, {commitment: "confirmed", maxSupportedTransactionVersion: 1}) .catch((e) => { throw new BasketError("Couldn't confirm transaction", txId); }) .then(response => { if (!response) return; if (response.meta && response.meta.err) result = false; else result = true; }) } if (result == undefined) return false; return result; } export async function sendSignedTransactions( connection: Connection, transactions: (Transaction|VersionedTransaction)[], confirmFirstN: number = 0, ): Promise<TransactionSignature[]> { if (transactions.length == 0) return []; if (!transactions) return ["SignatureError"]; let txs: TransactionSignature[] = []; for (let i = 0; i < confirmFirstN; i++) { let txId = await sendSignedTransaction(connection, transactions[i]); await confirmTransaction(connection, txId).catch((e) => { console.log(txId, e.message, "Couldn't confirm"); return true; }) txs.push(txId); } let remainingTxs = await Promise.all( transactions .slice(confirmFirstN, transactions.length) .map(transaction => sendSignedTransaction(connection, transaction).catch((e) => "Error")) ); // await Promise.all(remainingTxs.map(tx => confirmTransaction(connection, tx))).catch(() => {}); return [...txs, ...remainingTxs]; } export const getAddressLookupTableAccounts = async ( connection: Connection, keys: string[] ): Promise<AddressLookupTableAccount[]> => { const addressLookupTableAccountInfos = await connection.getMultipleAccountsInfo( keys.map((key) => new PublicKey(key)) ); return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => { const addressLookupTableAddress = keys[index]; if (accountInfo) { let state: AddressLookupTableState = AddressLookupTableAccount.deserialize(accountInfo.data); const addressLookupTableAccount = new AddressLookupTableAccount({ key: new PublicKey(addressLookupTableAddress), state: state, }); acc.push(addressLookupTableAccount); } return acc; }, new Array<AddressLookupTableAccount>()); }; export async function buildTxFromQuoteResponse( quoteResponse: any, rebalanceInfo: RebalanceInfo, connection: Connection, jupAPIkey: string, ): Promise<JupSwapData> { const txData = (await axios.post( jupAPIkey + "swap-instructions", JSON.stringify({ quoteResponse, userPublicKey: BASKETS_PROGRAM_PDA.toBase58(), // feeAccount: REBALANCE_FEE_ACCOUNT.toBase58(), wrapAndUnwrapSol: false, }), { headers: { 'Content-Type': 'application/json' } } )).data; let { addressLookupTableAddresses, swapInstruction } = txData; let from = getAssociatedTokenAddressSync( new PublicKey(quoteResponse.inputMint), BASKETS_PROGRAM_PDA, true, ); let to = getAssociatedTokenAddressSync( new PublicKey(quoteResponse.outputMint), BASKETS_PROGRAM_PDA, true, ); for (let i = 0; i < swapInstruction.accounts.length; i++) { if (swapInstruction.accounts[i].pubkey == BASKETS_PROGRAM_PDA.toBase58()) swapInstruction.accounts[i].isSigner = false; if (swapInstruction.accounts[i].pubkey == from.toBase58()) swapInstruction.accounts[i].pubkey = rebalanceInfo.tokenAccountFrom; if (swapInstruction.accounts[i].pubkey == to.toBase58()) swapInstruction.accounts[i].pubkey = rebalanceInfo.tokenAccountTo; } swapInstruction.accounts = swapInstruction.accounts.map((x: any) => { return { ...x, pubkey: new PublicKey(x.pubkey)}}); let data: Buffer = Buffer.from(swapInstruction.data, "base64"); let dataLength = data.length - 19; let fromAmount = parseInt(new BN(data.slice(dataLength, dataLength + 8),"le").toString()); let toAmount = parseInt(new BN(data.slice(dataLength + 8, dataLength + 16),"le").toString()); let slippageBps = new BN(dataLength + 16, dataLength + 18, "le").toNumber(); let feeBps = new BN(data.slice(dataLength + 18, dataLength + 19), "le").toNumber(); data = Buffer.concat([data.slice(0, dataLength), Buffer.alloc(128 - dataLength)]) let lookupTableAccounts = await getAddressLookupTableAccounts(connection, addressLookupTableAddresses); return { type: "Simple", programId: new PublicKey(swapInstruction.programId), accounts: swapInstruction.accounts, firstIxEnd: dataLength, firstIxAccounts: swapInstruction.accounts.length, dataLength: dataLength, data: data, fromTokenId: rebalanceInfo.side == Side.To ? rebalanceInfo.tokenId : 0, toTokenId: rebalanceInfo.side == Side.From ? rebalanceInfo.tokenId : 0, midTokenPda: "", fromAmount: fromAmount, toAmount: toAmount, slippageBps: slippageBps, feeBps: feeBps, lookupTableAccounts: lookupTableAccounts, }; } export async function findRoute( rebalanceInfo: any, jupAPIkey: string, slippage: number, midToken: string, inputAmountOverwrite: number ): Promise<any> { let res: { inputAmount: number, outputAmount: number, quotes: any[], } = { inputAmount: 0, outputAmount: 0, quotes: [] }; try { let quoteResponse = (await axios.get( jupAPIkey + "quote" + "?inputMint=" + rebalanceInfo.mintFrom + "&outputMint=" + rebalanceInfo.mintTo + "&amount=" + inputAmountOverwrite + "&slippageBps=" + slippage + // "&platformFeeBps=" + 5 + "&onlyDirectRoutes=true" )).data; res.inputAmount = inputAmountOverwrite; res.outputAmount = parseInt(quoteResponse.outAmount); res.quotes = [quoteResponse]; } catch {} try { let route1 = (await axios.get( jupAPIkey + "quote" + "?inputMint=" + rebalanceInfo.mintFrom + "&outputMint=" + midToken + "&amount=" + inputAmountOverwrite + "&slippageBps=" + slippage + // "&platformFeeBps=" + 5 + "&onlyDirectRoutes=true" )).data; let route2 = (await axios.get( jupAPIkey + "quote" + "?inputMint=" + midToken + "&outputMint=" + rebalanceInfo.mintTo + "&amount=" + route1.outAmount + "&slippageBps=" + slippage + // "&platformFeeBps=" + 5 + "&onlyDirectRoutes=true" )).data; if (res.outputAmount <= parseInt(route2.outAmount)) { res.inputAmount = inputAmountOverwrite; res.outputAmount = parseInt(route2.outAmount); res.quotes = [route1, route2]; } } catch {} return res; } export async function generateJupSwapInstruction( rebalanceInfo: RebalanceInfo, slippage: number, connection: Connection, fromPriceWithDecimals: number, toPriceWithDecimals: number, midTokenMint: string, midTokenPda: string, jupAPIkey: string, ): Promise<JupSwapData> { let l = 1, r = rebalanceInfo.amountFrom, m = 0; let res = await findRoute( rebalanceInfo, jupAPIkey, slippage, midTokenMint, rebalanceInfo.amountFrom ); let inputValue = rebalanceInfo.amountFrom * fromPriceWithDecimals; let outputValue = res.outputAmount * toPriceWithDecimals; if (inputValue * (10000 - slippage) / 10000 > outputValue && "https://quote-api.jup.ag/v6/" != jupAPIkey) { for (let it = 0; it < 3; it++) { m = (l+r) >> 1; let check = await findRoute( rebalanceInfo, jupAPIkey, slippage, midTokenMint, m ); let inputValue = m * fromPriceWithDecimals; let outputValue = check.outputAmount * toPriceWithDecimals; if (inputValue * (10000 - slippage) / 10000 >= outputValue) r = m; else { l = m; res = check; } if ((r-l) * fromPriceWithDecimals <= 5) break; // No point in iterating within 2USD } } if (res.outputAmount == 0) throw new Error("Couldn't find quote within slippage"); if (res.quotes.length == 2) { let data = await Promise.all([ await buildTxFromQuoteResponse( res.quotes[0], { ...rebalanceInfo, tokenAccountTo: midTokenPda }, connection, jupAPIkey ), await buildTxFromQuoteResponse( res.quotes[1], { ...rebalanceInfo, tokenAccountFrom: midTokenPda }, connection, jupAPIkey ) ]); let combined = { type: "Transitive", programId: new PublicKey(data[0].programId), accounts: [...data[0].accounts, ...data[1].accounts], firstIxEnd: data[0].dataLength, dataLength: data[0].dataLength + data[1].dataLength, firstIxAccounts: data[0].accounts.length, data: Buffer.concat([ data[0].data.slice(0, data[0].dataLength), data[1].data.slice(0, data[1].dataLength), Buffer.alloc(128 - data[0].dataLength - data[1].dataLength) ]), fromTokenId: rebalanceInfo.side == Side.To ? rebalanceInfo.tokenId : 0, toTokenId: rebalanceInfo.side == Side.From ? rebalanceInfo.tokenId : 0, midTokenPda: midTokenPda, fromAmount: data[0].fromAmount, toAmount: data[1].toAmount, slippageBps: data[1].slippageBps, feeBps: data[1].feeBps, lookupTableAccounts: [...data[0].lookupTableAccounts, ...data[1].lookupTableAccounts], } return combined; } let data = await buildTxFromQuoteResponse( res.quotes[0], rebalanceInfo, connection, jupAPIkey ) return data; } export async function generateJupTxData( signer: PublicKey, mintFrom: string, mintTo: string, amountFrom: number, maxAllowedAccounts: number, slippage: number, fromPriceWithDecimals: number, toPriceWithDecimals: number, jupAPIkey: string, ): Promise<any> { // Helper function to get a quote from Jupiter API const getQuote = async (amount: number) => { let requestURL = `${jupAPIkey}quote?inputMint=${mintFrom}&outputMint=${mintTo}&amount=${amount}&slippageBps=${slippage + 50}`; if (maxAllowedAccounts != 64) requestURL += `&maxAccounts=${maxAllowedAccounts}` const response = await axios.get(requestURL).catch(e => { console.log("Jup quote error"); console.log(e.message); console.log("--------"); return ({ data: null }) }); return response.data; }; let swapValue = 0; // Get initial quote let res = await getQuote(amountFrom); if (!res) return null; const inputValue = amountFrom * fromPriceWithDecimals; const outputValue = res.outAmount * toPriceWithDecimals; console.log("##### Checking Routes"); console.log(mintFrom, mintTo, (amountFrom * fromPriceWithDecimals).toFixed(2), (res.outAmount * toPriceWithDecimals).toFixed(2), (outputValue / inputValue).toFixed(6)); // Check if the swap meets the slippage requirement if (inputValue * (10000 - slippage) / 10000 > outputValue) { // Binary search for the optimal amount let l = 1, r = amountFrom; for (let it = 0; it < 5; it++) { if ((r - l) * fromPriceWithDecimals <= 1) break; // Stop if the difference is less than $1 const m = Math.floor((l + r) / 2); const check = await getQuote(m); if (!check) { r = m; continue; } const checkInputValue = m * fromPriceWithDecimals; const checkOutputValue = check.outAmount * toPriceWithDecimals; console.log(checkInputValue.toFixed(2), checkOutputValue.toFixed(2), (checkOutputValue / checkInputValue).toFixed(6)); if (checkInputValue * (10000 - slippage) / 10000 > checkOutputValue) { r = m; } else { l = m; res = check; swapValue = checkInputValue; break; } } } else swapValue = inputValue; if (!res) return null; // Get swap instructions from Jupiter API const txData = await axios.post( `${jupAPIkey}swap-instructions`, { quoteResponse: res, userPublicKey: signer.toBase58(), wrapAndUnwrapSol: false, }, { headers: { 'Content-Type': 'application/json' } } ).then(response => response.data); return { ...txData, tokenAmount: res.inAmount, swapValue: swapValue, res }; } export function calculateRebalanceAmounts( program: Program<BasketsIDL>, numTokens: number, timestamp: number, lastRebalanceTime: number[], rebalanceInterval: number, currentCompToken: number[], currentCompAmount: number[], targetWeights: number[], weightSum: number, tokenList: TokenSettings[], rebalanceThreshold: number, oraclePriceData: number[], forceRebalance: boolean, ): RebalanceInfo[] { let currentValues: number[] = []; let basketWorth: number = 0; for(let i=0; i<numTokens; i++){ let price = oraclePriceData[currentCompToken[i]]; let tokenAmount = currentCompAmount[i] / 10 ** tokenList[currentCompToken[i]].decimals; let tokenValue = price * tokenAmount; currentValues.push(tokenValue); basketWorth += tokenValue; } let rebalanceInfos: RebalanceInfo[] = []; if (basketWorth == 0) return rebalanceInfos; for(let i = 1; i < numTokens; i++) { let currentPercentage = (basketWorth > 0) ? currentValues[i] / basketWorth : 0; let targetPercentage = (weightSum > 0) ? targetWeights[i] / weightSum : 0; if (lastRebalanceTime[i] + rebalanceInterval > timestamp && (!forceRebalance)) continue; if (currentPercentage > targetPercentage * (1 + rebalanceThreshold / 10000) || forceRebalance ) rebalanceInfos.push({ tokenId: currentCompToken[i], tokenAccountFrom: tokenList[currentCompToken[i]].pdaTokenAccount, mintFrom: tokenList[currentCompToken[i]].tokenMint, oracleFrom: tokenList[currentCompToken[i]].oracleAccount, tokenAccountTo: tokenList[0].pdaTokenAccount, mintTo: tokenList[0].tokenMint, oracleTo: tokenList[0].oracleAccount, amountFrom: Math.floor( currentCompAmount[i] * (1 - targetPercentage / currentPercentage) ), decimals: tokenList[currentCompToken[i]].decimals, volume: currentValues[i] - targetPercentage * basketWorth, side: Side.To, }) if (currentPercentage < targetPercentage * (1 - rebalanceThreshold / 10000) || forceRebalance) rebalanceInfos.push({ tokenId: currentCompToken[i], tokenAccountTo: tokenList[currentCompToken[i]].pdaTokenAccount, mintTo: tokenList[currentCompToken[i]].tokenMint, oracleTo: tokenList[currentCompToken[i]].oracleAccount, tokenAccountFrom: tokenList[0].pdaTokenAccount, mintFrom: tokenList[0].tokenMint, oracleFrom: tokenList[0].oracleAccount, amountFrom: Math.floor( (targetPercentage * basketWorth - currentValues[i]) * 10 ** tokenList[0].decimals ), decimals: tokenList[0].decimals, volume: (targetPercentage * basketWorth - currentValues[i]), side: Side.From }) } return rebalanceInfos.filter(x => x.volume > 0.005); } export function stringToAscii( coingeckoId: string, ): Array<number> { let coingeckoIdAscii = []; for(let i=0; i<coingeckoId.length; i++) coingeckoIdAscii.push(coingeckoId[i].charCodeAt(0)); while (coingeckoIdAscii.length != 30) coingeckoIdAscii.push(0); return coingeckoIdAscii; } export function asciiToString( coingeckoIdAscii: number[], ): string { let coingeckoId: string = ""; for(let i=0; i<coingeckoIdAscii.length; i++) if(coingeckoIdAscii[i] != 0) coingeckoId += String.fromCharCode(coingeckoIdAscii[i]).toString(); return coingeckoId; } export async function fetchTokenList( program: Program<BasketsIDL>, ): Promise<TokenSettings[]> { let solanaTokenList = (await axios.get( "https://cache.symmetry.fi/tokenlist.json" )).data; let tokenMap: any = {}; for (let i = 0; i < solanaTokenList.length; i++) tokenMap[solanaTokenList[i].address] = { symbol: solanaTokenList[i].symbol, name: solanaTokenList[i].name, decimals: solanaTokenList[i].decimals, } let state = await program.account.tokenList.fetch(TOKEN_LIST_ADDRESS, "confirmed"); let numTokens = state.numTokens.toNumber(); let tokens = []; for (let i = 0; i < numTokens; i++) { //@ts-ignore let tokenSettings = state.list[i]; tokens.push({ id: i, symbol: tokenMap[tokenSettings.tokenMint.toBase58()]? tokenMap[tokenSettings.tokenMint.toBase58()].symbol : undefined, name: tokenMap[tokenSettings.tokenMint.toBase58()] ? tokenMap[tokenSettings.tokenMint.toBase58()].name : undefined, tokenMint: tokenSettings.tokenMint.toBase58(), decimals: tokenSettings.decimals, coingeckoId: asciiToString(tokenSettings.coingeckoId), pdaTokenAccount: tokenSettings.pdaTokenAccount.toBase58(), oracleType: ["Pyth", "Switchboard", "LST", "PythSponsored", "SwbOnDemand"][tokenSettings.oracleType], oracleAccount: tokenSettings.oracleAccount.toBase58(), oracleIndex: tokenSettings.oracleIndex, oracleConfidencePct: tokenSettings.oracleConfidencePct, fixedConfidenceBps: tokenSettings.fixedConfidenceBps, tokenSwapFeeBeforeTwBps: tokenSettings.tokenSwapFeeBeforeTwBps, tokenSwapFeeAfterTwBps: tokenSettings.tokenSwapFeeAfterTwBps, isLive: tokenSettings.isLive == 0 ? false : true, lpOn: tokenSettings.lpOn == 0 ? false : true, useCurveData: tokenSettings.useCurveData == 0 ? false : true, additionalData: tokenSettings.additionalData, }); } return tokens; } export function getCurrentComposition( basket: Basket, tokenList: TokenSettings[], oraclePriceData: number[] ): any { let basketWorth = 0; let currentComp = []; for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++) { let token = basket.data.currentCompToken[i].toNumber(); let amount = parseInt(basket.data.currentCompAmount[i].toString()) / 10 ** tokenList[token].decimals; basketWorth += amount * oraclePriceData[token]; currentComp.push({ mintAddress: tokenList[token].tokenMint, coingeckoId: tokenList[token].coingeckoId, symbol: tokenList[token].symbol, symmetryTokenId: token, lockedAmount: amount, oraclePrice: oraclePriceData[token], usdValue: amount * oraclePriceData[token], currentWeight: 0, targetWeight: Number(basket.data.targetWeight[i].toNumber() * 100 / basket.data.weightSum.toNumber()), tokenData: tokenList[token], }) } for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++) currentComp[i].currentWeight = currentComp[i].usdValue * 100 / basketWorth; let symbol = asciiToString(basket.data.symbol.slice(0, basket.data.symbolLength)); let name = asciiToString(basket.data.name.slice(0, basket.data.nameLength)); let uri = asciiToString(basket.data.uri.slice(0, basket.data.uriLength)); let basketInfo = { symbol: symbol, name: name, uri: uri, currentSupply: basket.data.supplyOutstanding.toNumber() / 10 ** 6, basketTokenMint: basket.data.fundToken.toBase58(), basketWorth: basketWorth, rawPrice: 10 ** 6 * basketWorth / basket.data.supplyOutstanding.toNumber(), manager: basket.data.manager.toBase58(), managerFee: basket.data.managerFee.toNumber() / 100, activelyManaged: basket.data.activelyManaged.toNumber(), host: basket.data.hostPubkey.toBase58(), hstFee: basket.data.hostFee.toNumber(), currentComposition: currentComp, rules: basket.data.rules.slice(0, basket.data.numOfRules.toNumber()).map(rule => { return { totalWeight: rule.totalWeight.toNumber(), filterBy: rule.filterBy.toNumber(), filterDays: rule.filterDays.toNumber(), sortBy: rule.sortBy.toNumber(), weightBy: rule.weightBy.toNumber(), weightDays: rule.weightDays.toNumber(), weightExpo: rule.weightExpo.toNumber() / 100, fixedAsset: tokenList[rule.fixedAsset.toNumber()], ruleAssets: rule.ruleAssets .slice(0, rule.numAssets.toNumber()) .map(x => tokenList[x.toNumber()]) } }) }; return basketInfo; } export async function validateCreateBasketParams(createBasketParams: CreateBasketParams, tokenList: TokenSettings[], basketState?: PublicKey, skipNameCheck: boolean = false) { if (createBasketParams.name.length > 60) throw new BasketError("Basket Name should be at most 60 characters"); if (createBasketParams.symbol.length > 10) throw new BasketError("Basket Symbol should be at most 10 characters"); if (createBasketParams.uri.length > 300) throw new BasketError("Basket MetadataURI should be at most 300 characters"); if (createBasketParams.refilterInterval < 24 * 60 * 60) throw new BasketError("Minimum refilter interval should be 24 hours"); if (createBasketParams.reweightInterval < 60 * 60) throw new BasketError("Minimum reweight interval should be 1 hour"); if (createBasketParams.rebalanceInterval < 60 * 60) throw new BasketError("Minimum rebalance interval should be 1 hour"); if (createBasketParams.rebalanceThreshold > BPS_DIVIDER) throw new BasketError("Maximum rebalance threshold should be 10000 bps"); if (createBasketParams.rebalanceSlippage > BPS_DIVIDER) throw new BasketError("Maximum rebalance slippage should be 10000 bps"); if (createBasketParams.lpOffsetThreshold > BPS_DIVIDER) throw new BasketError("Maximum lp offset threshold should be 10000 bps"); if (createBasketParams.managerFee > BPS_DIVIDER) throw new BasketError("Maximum manager fee should be 10000 bps"); if (createBasketParams.hostPlatformFee > BPS_DIVIDER) throw new BasketError("Maximum host platform fee should be 10000 bps"); let totalAssets = 1; for (let i = 0; i < createBasketParams.rules.length; i++) { if (createBasketParams.rules[i].totalWeight <= 0) throw new BasketError("Total weight of each rule should be positive"); if (createBasketParams.rules[i].totalWeight > 1000) throw new BasketError("Maximum total weight of each roule should be 1000"); if (createBasketParams.rules[i].weightExpo < 0) throw new BasketError("Rule weight expo should be positive"); if (createBasketParams.rules[i].weightExpo > 100 && createBasketParams.rules[i].weightBy != WeightType.Performance) throw new BasketError("Maxiumum rule weight expo should be 100"); if (createBasketParams.rules[i].weightExpo > 1000) throw new BasketError("Maxiumum rule weight expo for performance should be 1000"); if (createBasketParams.rules[i].weightBy > 3 || createBasketParams.rules[i].filterBy > 3) throw new BasketError("FilterBy and WeightBy should be in range 0..3"); if (createBasketParams.rules[i].weightDays > 5 || createBasketParams.rules[i].filterDays > 5) throw new BasketError("FilterDays and WeightDays should be in range 0..5"); if (createBasketParams.rules[i].sortBy > 1) throw new BasketError("SortBy should be in range 0..1"); if (createBasketParams.rules[i].filterBy == FilterType.Fixed) { if (tokenList.find(token => token.id == createBasketParams.rules[i].fixedAsset)?.isLive == false) throw new BasketError("One of the fixed assets is not currently supported."); if ((createBasketParams.assetPool.find(x => x == createBasketParams.rules[i].fixedAsset)) == undefined) throw new BasketError("One of the fixed assets is not present in the asset pool."); totalAssets += 1; continue; } else totalAssets += createBasketParams.rules[i].numAssets; } createBasketParams.assetPool = createBasketParams.assetPool.sort(); if (createBasketParams.assetPool[0] != 0 || createBasketParams.assetPool.length != new Set(createBasketParams.assetPool).size) throw new BasketError("Asset pool should contain USDC and shouldn't contain repeating tokens"); if (createBasketParams.assetPool.find(x => tokenList.find(token => token.id == x)?.isLive == false)) throw new BasketError("One of the tokens in the asset pool is not currently supported."); if (totalAssets > COMBINED_TOKENS_IN_A_BASKET) throw new BasketError("Maximum allowed number of assets in a basket is " + COMBINED_TOKENS_IN_A_BASKET); if (skipNameCheck) return; let check = await axios.post( "https://api.symmetry.fi/v1/funds-name-setter", JSON.stringify({ command: "check_exists", name: createBasketParams.name, symbol: createBasketParams.symbol, description: createBasketParams.uri, }) ); if (check.data.status == "fail" && check.data.msg) throw new BasketError(check.data.msg); if (check.data.status != "ok") throw new BasketError("Validation failed"); let basketStatePubkey = basketState ? basketState.toBase58() : ""; if ( (check.data.name_owner != null && check.data.name_owner != basketStatePubkey) || (check.data.symbol_owner != null && check.data.symbol_owner != basketStatePubkey) ) throw new BasketError("Basket with given name or symbol already exists."); } export async function tryMetadata(parsed: any) { let metadata = undefined; try { if (parsed.uri) { let req = await fetch(parsed.uri); let json = await req.json(); metadata = json; } } catch {} return metadata; } async function getBasketTokenMint( connection: Connection, basket: PublicKey ): Promise<PublicKey> { let provider = new AnchorProvider( connection, new NodeWallet(Keypair.generate()), {commitment: "processed"} ); let program = new Program<BasketsIDL>(IDL, provider); let basketObj = await Basket.loadFromPubkey(program, basket); return basketObj.data.fundToken; } async function initATAForUserOrWrapSolIxs( connection: Connection, user: PublicKey, tokenMint: PublicKey, lamports: number = 0 ): Promise<TransactionInstruction[]> { let ixs = []; let ata = getAssociatedTokenAddressSync( tokenMint, user, ); let infoAta = await connection.getMultipleAccountsInfo([ata]); if (!infoAta[0]) ixs.push( createAssociatedTokenAccountInstruction( user, ata, user, tokenMint, ) ); if (tokenMint.toBase58() == NATIVE_MINT.toBase58()) { //@ts-ignore let info: AccountInfo<Buffer> = infoAta[0]; let toDeposit = lamports; if (info) { let parsedInfo = AccountLayout.decode(info.data); toDeposit -= parseInt(parsedInfo.amount.toString()); } if (toDeposit > 0) { ixs.push( SystemProgram.transfer({ fromPubkey: user, toPubkey: ata, lamports: toDeposit }), ); ixs.push( createSyncNativeInstruction(ata, TOKEN_PROGRAM_ID) ); } } return ixs; }