@symmetry-hq/baskets-sdk
Version:
Software Development Kit for interacting with Symmetry Baskets Program
766 lines (765 loc) • 41.2 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAddressLookupTableAccounts = void 0;
exports.delay = delay;
exports.rawOraclePrices = rawOraclePrices;
exports.getOraclePrices = getOraclePrices;
exports.getFilteredProgramAccounts = getFilteredProgramAccounts;
exports.signVersionedTransactions = signVersionedTransactions;
exports.signTransactionsWithWallet = signTransactionsWithWallet;
exports.sendSignedTransaction = sendSignedTransaction;
exports.confirmTransaction = confirmTransaction;
exports.sendSignedTransactions = sendSignedTransactions;
exports.buildTxFromQuoteResponse = buildTxFromQuoteResponse;
exports.findRoute = findRoute;
exports.generateJupSwapInstruction = generateJupSwapInstruction;
exports.generateJupTxData = generateJupTxData;
exports.calculateRebalanceAmounts = calculateRebalanceAmounts;
exports.stringToAscii = stringToAscii;
exports.asciiToString = asciiToString;
exports.fetchTokenList = fetchTokenList;
exports.getCurrentComposition = getCurrentComposition;
exports.validateCreateBasketParams = validateCreateBasketParams;
exports.tryMetadata = tryMetadata;
const anchor_1 = require("@coral-xyz/anchor");
const web3_js_1 = require("@solana/web3.js");
const basketsIDL_1 = require("./basketsIDL");
const config_1 = require("./config");
const client_1 = require("@pythnetwork/client");
const axios_1 = __importDefault(require("axios"));
const basketState_1 = require("./basketState");
const nodewallet_1 = __importDefault(require("@coral-xyz/anchor/dist/cjs/nodewallet"));
const splTokenHelpers_1 = require("./splTokenHelpers");
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function rawOraclePrices(connection, tokenList) {
return __awaiter(this, void 0, void 0, function* () {
let oraclePricesData = yield connection
.getMultipleAccountsInfo(tokenList.map((x) => 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((0, client_1.parsePriceData)(oraclePricesData[i].data).aggregate.price);
else if (tokenList[i].oracleType == 1) {
let price = parseInt(new anchor_1.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 anchor_1.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 anchor_1.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 = 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 anchor_1.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;
});
}
function getOraclePrices(program, tokenList) {
return __awaiter(this, void 0, void 0, function* () {
let oraclePricesData = (yield program.provider.connection
.getMultipleAccountsInfo(tokenList.map(x => new web3_js_1.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((0, client_1.parsePriceData)(oraclePricesData[i].data).aggregate.price);
else if (tokenList[i].oracleType == "Switchboard") {
let price = parseInt(new anchor_1.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 anchor_1.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 anchor_1.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 = 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 anchor_1.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;
});
}
function getFilteredProgramAccounts(connection, filters) {
return __awaiter(this, void 0, void 0, function* () {
return yield connection
.getProgramAccounts(config_1.BASKETS_PROGRAM_ID, {
commitment: "confirmed",
filters,
encoding: 'base64'
});
});
}
function signVersionedTransactions(wallet, transactions) {
return __awaiter(this, void 0, void 0, function* () {
let txs = [];
// @ts-ignore
txs = yield 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) {
console.log("Couldn't sign transactions: " + e.message);
}
}
return txs;
});
}
function signTransactionsWithWallet(connection, wallet, transactionsData) {
return __awaiter(this, void 0, void 0, function* () {
if (transactionsData.length == 0)
return [];
let { blockhash } = yield 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 yield wallet.signAllTransactions(transactionsData.map(data => data.transaction)).catch((e) => { throw new config_1.BasketError("Couldn't sign transactions: " + e.message); });
});
}
function sendSignedTransaction(connection_1, transaction_1) {
return __awaiter(this, arguments, void 0, function* (connection, transaction, retries = 2, delayMs = 400) {
let serialized = transaction.serialize();
let txId = yield connection.sendRawTransaction(serialized, { skipPreflight: true, preflightCommitment: "processed", maxRetries: 3 }).catch((e) => { throw new config_1.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;
});
}
function confirmTransaction(connection_1, txId_1) {
return __awaiter(this, arguments, void 0, function* (connection, txId, timeout = 30) {
let result = undefined;
for (let _ = 0; _ < timeout && result == undefined; _++) {
yield delay(1000);
yield connection
.getTransaction(txId, { commitment: "confirmed", maxSupportedTransactionVersion: 1 })
.catch((e) => { throw new config_1.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;
});
}
function sendSignedTransactions(connection_1, transactions_1) {
return __awaiter(this, arguments, void 0, function* (connection, transactions, confirmFirstN = 0) {
if (transactions.length == 0)
return [];
if (!transactions)
return ["SignatureError"];
let txs = [];
for (let i = 0; i < confirmFirstN; i++) {
let txId = yield sendSignedTransaction(connection, transactions[i]);
yield confirmTransaction(connection, txId).catch((e) => {
console.log(txId, e.message, "Couldn't confirm");
return true;
});
txs.push(txId);
}
let remainingTxs = yield 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];
});
}
const getAddressLookupTableAccounts = (connection, keys) => __awaiter(void 0, void 0, void 0, function* () {
const addressLookupTableAccountInfos = yield connection.getMultipleAccountsInfo(keys.map((key) => new web3_js_1.PublicKey(key)));
return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => {
const addressLookupTableAddress = keys[index];
if (accountInfo) {
let state = web3_js_1.AddressLookupTableAccount.deserialize(accountInfo.data);
const addressLookupTableAccount = new web3_js_1.AddressLookupTableAccount({
key: new web3_js_1.PublicKey(addressLookupTableAddress),
state: state,
});
acc.push(addressLookupTableAccount);
}
return acc;
}, new Array());
});
exports.getAddressLookupTableAccounts = getAddressLookupTableAccounts;
function buildTxFromQuoteResponse(quoteResponse, rebalanceInfo, connection, jupAPIkey) {
return __awaiter(this, void 0, void 0, function* () {
const txData = (yield axios_1.default.post(jupAPIkey + "swap-instructions", JSON.stringify({
quoteResponse,
userPublicKey: config_1.BASKETS_PROGRAM_PDA.toBase58(),
// feeAccount: REBALANCE_FEE_ACCOUNT.toBase58(),
wrapAndUnwrapSol: false,
}), { headers: { 'Content-Type': 'application/json' } })).data;
let { addressLookupTableAddresses, swapInstruction } = txData;
let from = (0, splTokenHelpers_1.getAssociatedTokenAddressSync)(new web3_js_1.PublicKey(quoteResponse.inputMint), config_1.BASKETS_PROGRAM_PDA, true);
let to = (0, splTokenHelpers_1.getAssociatedTokenAddressSync)(new web3_js_1.PublicKey(quoteResponse.outputMint), config_1.BASKETS_PROGRAM_PDA, true);
for (let i = 0; i < swapInstruction.accounts.length; i++) {
if (swapInstruction.accounts[i].pubkey == config_1.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) => { return Object.assign(Object.assign({}, x), { pubkey: new web3_js_1.PublicKey(x.pubkey) }); });
let data = Buffer.from(swapInstruction.data, "base64");
let dataLength = data.length - 19;
let fromAmount = parseInt(new anchor_1.BN(data.slice(dataLength, dataLength + 8), "le").toString());
let toAmount = parseInt(new anchor_1.BN(data.slice(dataLength + 8, dataLength + 16), "le").toString());
let slippageBps = new anchor_1.BN(dataLength + 16, dataLength + 18, "le").toNumber();
let feeBps = new anchor_1.BN(data.slice(dataLength + 18, dataLength + 19), "le").toNumber();
data = Buffer.concat([data.slice(0, dataLength), Buffer.alloc(128 - dataLength)]);
let lookupTableAccounts = yield (0, exports.getAddressLookupTableAccounts)(connection, addressLookupTableAddresses);
return {
type: "Simple",
programId: new web3_js_1.PublicKey(swapInstruction.programId),
accounts: swapInstruction.accounts,
firstIxEnd: dataLength,
firstIxAccounts: swapInstruction.accounts.length,
dataLength: dataLength,
data: data,
fromTokenId: rebalanceInfo.side == config_1.Side.To ? rebalanceInfo.tokenId : 0,
toTokenId: rebalanceInfo.side == config_1.Side.From ? rebalanceInfo.tokenId : 0,
midTokenPda: "",
fromAmount: fromAmount,
toAmount: toAmount,
slippageBps: slippageBps,
feeBps: feeBps,
lookupTableAccounts: lookupTableAccounts,
};
});
}
function findRoute(rebalanceInfo, jupAPIkey, slippage, midToken, inputAmountOverwrite) {
return __awaiter(this, void 0, void 0, function* () {
let res = {
inputAmount: 0,
outputAmount: 0,
quotes: []
};
try {
let quoteResponse = (yield axios_1.default.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 (_a) { }
try {
let route1 = (yield axios_1.default.get(jupAPIkey + "quote" +
"?inputMint=" + rebalanceInfo.mintFrom +
"&outputMint=" + midToken +
"&amount=" + inputAmountOverwrite +
"&slippageBps=" + slippage +
// "&platformFeeBps=" + 5 +
"&onlyDirectRoutes=true")).data;
let route2 = (yield axios_1.default.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 (_b) { }
return res;
});
}
function generateJupSwapInstruction(rebalanceInfo, slippage, connection, fromPriceWithDecimals, toPriceWithDecimals, midTokenMint, midTokenPda, jupAPIkey) {
return __awaiter(this, void 0, void 0, function* () {
let l = 1, r = rebalanceInfo.amountFrom, m = 0;
let res = yield 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 = yield 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 = yield Promise.all([
yield buildTxFromQuoteResponse(res.quotes[0], Object.assign(Object.assign({}, rebalanceInfo), { tokenAccountTo: midTokenPda }), connection, jupAPIkey),
yield buildTxFromQuoteResponse(res.quotes[1], Object.assign(Object.assign({}, rebalanceInfo), { tokenAccountFrom: midTokenPda }), connection, jupAPIkey)
]);
let combined = {
type: "Transitive",
programId: new web3_js_1.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 == config_1.Side.To ? rebalanceInfo.tokenId : 0,
toTokenId: rebalanceInfo.side == config_1.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 = yield buildTxFromQuoteResponse(res.quotes[0], rebalanceInfo, connection, jupAPIkey);
return data;
});
}
function generateJupTxData(signer, mintFrom, mintTo, amountFrom, maxAllowedAccounts, slippage, fromPriceWithDecimals, toPriceWithDecimals, jupAPIkey) {
return __awaiter(this, void 0, void 0, function* () {
// Helper function to get a quote from Jupiter API
const getQuote = (amount) => __awaiter(this, void 0, void 0, function* () {
let requestURL = `${jupAPIkey}quote?inputMint=${mintFrom}&outputMint=${mintTo}&amount=${amount}&slippageBps=${slippage + 50}`;
if (maxAllowedAccounts != 64)
requestURL += `&maxAccounts=${maxAllowedAccounts}`;
const response = yield axios_1.default.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 = yield 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 = yield 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 = yield axios_1.default.post(`${jupAPIkey}swap-instructions`, {
quoteResponse: res,
userPublicKey: signer.toBase58(),
wrapAndUnwrapSol: false,
}, { headers: { 'Content-Type': 'application/json' } }).then(response => response.data);
return Object.assign(Object.assign({}, txData), { tokenAmount: res.inAmount, swapValue: swapValue, res });
});
}
function calculateRebalanceAmounts(program, numTokens, timestamp, lastRebalanceTime, rebalanceInterval, currentCompToken, currentCompAmount, targetWeights, weightSum, tokenList, rebalanceThreshold, oraclePriceData, forceRebalance) {
let currentValues = [];
let basketWorth = 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 = [];
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: config_1.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: config_1.Side.From
});
}
return rebalanceInfos.filter(x => x.volume > 0.005);
}
function stringToAscii(coingeckoId) {
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;
}
function asciiToString(coingeckoIdAscii) {
let coingeckoId = "";
for (let i = 0; i < coingeckoIdAscii.length; i++)
if (coingeckoIdAscii[i] != 0)
coingeckoId += String.fromCharCode(coingeckoIdAscii[i]).toString();
return coingeckoId;
}
function fetchTokenList(program) {
return __awaiter(this, void 0, void 0, function* () {
let solanaTokenList = (yield axios_1.default.get("https://cache.symmetry.fi/tokenlist.json")).data;
let tokenMap = {};
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 = yield program.account.tokenList.fetch(config_1.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;
});
}
function getCurrentComposition(basket, tokenList, oraclePriceData) {
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;
}
function validateCreateBasketParams(createBasketParams_1, tokenList_1, basketState_2) {
return __awaiter(this, arguments, void 0, function* (createBasketParams, tokenList, basketState, skipNameCheck = false) {
var _a;
if (createBasketParams.name.length > 60)
throw new config_1.BasketError("Basket Name should be at most 60 characters");
if (createBasketParams.symbol.length > 10)
throw new config_1.BasketError("Basket Symbol should be at most 10 characters");
if (createBasketParams.uri.length > 300)
throw new config_1.BasketError("Basket MetadataURI should be at most 300 characters");
if (createBasketParams.refilterInterval < 24 * 60 * 60)
throw new config_1.BasketError("Minimum refilter interval should be 24 hours");
if (createBasketParams.reweightInterval < 60 * 60)
throw new config_1.BasketError("Minimum reweight interval should be 1 hour");
if (createBasketParams.rebalanceInterval < 60 * 60)
throw new config_1.BasketError("Minimum rebalance interval should be 1 hour");
if (createBasketParams.rebalanceThreshold > config_1.BPS_DIVIDER)
throw new config_1.BasketError("Maximum rebalance threshold should be 10000 bps");
if (createBasketParams.rebalanceSlippage > config_1.BPS_DIVIDER)
throw new config_1.BasketError("Maximum rebalance slippage should be 10000 bps");
if (createBasketParams.lpOffsetThreshold > config_1.BPS_DIVIDER)
throw new config_1.BasketError("Maximum lp offset threshold should be 10000 bps");
if (createBasketParams.managerFee > config_1.BPS_DIVIDER)
throw new config_1.BasketError("Maximum manager fee should be 10000 bps");
if (createBasketParams.hostPlatformFee > config_1.BPS_DIVIDER)
throw new config_1.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 config_1.BasketError("Total weight of each rule should be positive");
if (createBasketParams.rules[i].totalWeight > 1000)
throw new config_1.BasketError("Maximum total weight of each roule should be 1000");
if (createBasketParams.rules[i].weightExpo < 0)
throw new config_1.BasketError("Rule weight expo should be positive");
if (createBasketParams.rules[i].weightExpo > 100 && createBasketParams.rules[i].weightBy != config_1.WeightType.Performance)
throw new config_1.BasketError("Maxiumum rule weight expo should be 100");
if (createBasketParams.rules[i].weightExpo > 1000)
throw new config_1.BasketError("Maxiumum rule weight expo for performance should be 1000");
if (createBasketParams.rules[i].weightBy > 3 || createBasketParams.rules[i].filterBy > 3)
throw new config_1.BasketError("FilterBy and WeightBy should be in range 0..3");
if (createBasketParams.rules[i].weightDays > 5 || createBasketParams.rules[i].filterDays > 5)
throw new config_1.BasketError("FilterDays and WeightDays should be in range 0..5");
if (createBasketParams.rules[i].sortBy > 1)
throw new config_1.BasketError("SortBy should be in range 0..1");
if (createBasketParams.rules[i].filterBy == config_1.FilterType.Fixed) {
if (((_a = tokenList.find(token => token.id == createBasketParams.rules[i].fixedAsset)) === null || _a === void 0 ? void 0 : _a.isLive) == false)
throw new config_1.BasketError("One of the fixed assets is not currently supported.");
if ((createBasketParams.assetPool.find(x => x == createBasketParams.rules[i].fixedAsset)) == undefined)
throw new config_1.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 config_1.BasketError("Asset pool should contain USDC and shouldn't contain repeating tokens");
if (createBasketParams.assetPool.find(x => { var _a; return ((_a = tokenList.find(token => token.id == x)) === null || _a === void 0 ? void 0 : _a.isLive) == false; }))
throw new config_1.BasketError("One of the tokens in the asset pool is not currently supported.");
if (totalAssets > config_1.COMBINED_TOKENS_IN_A_BASKET)
throw new config_1.BasketError("Maximum allowed number of assets in a basket is " + config_1.COMBINED_TOKENS_IN_A_BASKET);
if (skipNameCheck)
return;
let check = yield axios_1.default.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 config_1.BasketError(check.data.msg);
if (check.data.status != "ok")
throw new config_1.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 config_1.BasketError("Basket with given name or symbol already exists.");
});
}
function tryMetadata(parsed) {
return __awaiter(this, void 0, void 0, function* () {
let metadata = undefined;
try {
if (parsed.uri) {
let req = yield fetch(parsed.uri);
let json = yield req.json();
metadata = json;
}
}
catch (_a) { }
return metadata;
});
}
function getBasketTokenMint(connection, basket) {
return __awaiter(this, void 0, void 0, function* () {
let provider = new anchor_1.AnchorProvider(connection, new nodewallet_1.default(web3_js_1.Keypair.generate()), { commitment: "processed" });
let program = new anchor_1.Program(basketsIDL_1.IDL, provider);
let basketObj = yield basketState_1.Basket.loadFromPubkey(program, basket);
return basketObj.data.fundToken;
});
}
function initATAForUserOrWrapSolIxs(connection_1, user_1, tokenMint_1) {
return __awaiter(this, arguments, void 0, function* (connection, user, tokenMint, lamports = 0) {
let ixs = [];
let ata = (0, splTokenHelpers_1.getAssociatedTokenAddressSync)(tokenMint, user);
let infoAta = yield connection.getMultipleAccountsInfo([ata]);
if (!infoAta[0])
ixs.push((0, splTokenHelpers_1.createAssociatedTokenAccountInstruction)(user, ata, user, tokenMint));
if (tokenMint.toBase58() == splTokenHelpers_1.NATIVE_MINT.toBase58()) {
//@ts-ignore
let info = infoAta[0];
let toDeposit = lamports;
if (info) {
let parsedInfo = splTokenHelpers_1.AccountLayout.decode(info.data);
toDeposit -= parseInt(parsedInfo.amount.toString());
}
if (toDeposit > 0) {
ixs.push(web3_js_1.SystemProgram.transfer({
fromPubkey: user,
toPubkey: ata,
lamports: toDeposit
}));
ixs.push((0, splTokenHelpers_1.createSyncNativeInstruction)(ata, splTokenHelpers_1.TOKEN_PROGRAM_ID));
}
}
return ixs;
});
}