@symmetry-hq/baskets-sdk
Version:
Software Development Kit for interacting with Symmetry Baskets Program
966 lines (902 loc) • 38.9 kB
text/typescript
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;
}