Files
solana-agent-kit/src/utils/flashUtils.ts
2025-01-07 15:12:29 +05:30

301 lines
8.1 KiB
TypeScript

import { HermesClient } from "@pythnetwork/hermes-client";
import { OraclePrice } from "flash-sdk";
import { AnchorProvider, BN, Wallet } from "@coral-xyz/anchor";
import {
PoolConfig,
Token,
Referral,
PerpetualsClient,
Privilege,
} from "flash-sdk";
import { Cluster, PublicKey, Connection, Keypair } from "@solana/web3.js";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { SolanaAgentKit } from "../index";
const POOL_NAMES = [
"Crypto.1",
"Virtual.1",
"Governance.1",
"Community.1",
"Community.2",
"Community.3",
];
const DEFAULT_CLUSTER: Cluster = "mainnet-beta";
export const POOL_CONFIGS = POOL_NAMES.map((f) =>
PoolConfig.fromIdsByName(f, DEFAULT_CLUSTER),
);
const DUPLICATE_TOKENS = POOL_CONFIGS.map((f) => f.tokens).flat();
const tokenMap = new Map();
for (const token of DUPLICATE_TOKENS) {
tokenMap.set(token.symbol, token);
}
export const ALL_TOKENS: Token[] = Array.from(tokenMap.values());
export const ALL_CUSTODIES = POOL_CONFIGS.map((f) => f.custodies).flat();
const PROGRAM_ID = POOL_CONFIGS[0].programId;
// CU for trade instructions
export const OPEN_POSITION_CU = 150_000;
export const CLOSE_POSITION_CU = 180_000;
const HERMES_URL = "https://hermes.pyth.network"; // Replace with the actual Hermes URL if different
// Create a map of symbol to Pyth price ID
const PRICE_FEED_IDS = ALL_TOKENS.reduce(
(acc, token) => {
acc[token.symbol] = token.pythPriceId;
return acc;
},
{} as { [key: string]: string },
);
const hermesClient = new HermesClient(HERMES_URL, {});
export interface PythPriceEntry {
price: OraclePrice;
emaPrice: OraclePrice;
isStale: boolean;
status: PriceStatus;
}
export enum PriceStatus {
Trading,
Unknown,
Halted,
Auction,
}
export const fetchOraclePrice = async (
symbol: string,
): Promise<PythPriceEntry> => {
const priceFeedId = PRICE_FEED_IDS[symbol];
if (!priceFeedId) {
throw new Error(`Price feed ID not found for symbol: ${symbol}`);
}
try {
const hermesPriceFeed = await hermesClient.getPriceFeeds({
query: symbol,
filter: "crypto",
});
if (!hermesPriceFeed || hermesPriceFeed.length === 0) {
throw new Error(`No price feed received for ${symbol}`);
}
const hemrmesPriceUdpate = await hermesClient.getLatestPriceUpdates(
[priceFeedId],
{
encoding: "hex",
parsed: true,
},
);
if (!hemrmesPriceUdpate.parsed) {
throw new Error(`No price feed received for ${symbol}`);
}
const hermesEma = hemrmesPriceUdpate.parsed[0].ema_price;
const hermesPrice = hemrmesPriceUdpate.parsed[0].price;
const hermesPriceOracle = new OraclePrice({
price: new BN(hermesPrice.price),
exponent: new BN(hermesPrice.expo),
confidence: new BN(hermesPrice.conf),
timestamp: new BN(hermesPrice.publish_time),
});
const hermesEmaOracle = new OraclePrice({
price: new BN(hermesEma.price),
exponent: new BN(hermesEma.expo),
confidence: new BN(hermesEma.conf),
timestamp: new BN(hermesEma.publish_time),
});
const token = ALL_TOKENS.find((t) => t.pythPriceId === priceFeedId);
if (!token) {
throw new Error(`Token not found for price feed ID: ${priceFeedId}`);
}
const status = !token.isVirtual ? PriceStatus.Trading : PriceStatus.Unknown;
const pythPriceEntry: PythPriceEntry = {
price: hermesPriceOracle,
emaPrice: hermesEmaOracle,
isStale: false,
status: status,
};
return pythPriceEntry;
} catch (error) {
console.error(`Error in fetchOraclePrice for ${symbol}:`, error);
throw error;
}
};
export interface MarketInfo {
[key: string]: {
tokenPair: string;
token: string;
side: string;
pool: string;
};
}
const marketSdkInfo: MarketInfo = {};
// Loop through POOL_CONFIGS to process each market
POOL_CONFIGS.forEach((poolConfig) => {
poolConfig.markets.forEach((market) => {
const targetToken = ALL_TOKENS.find(
(token) => token.mintKey.toString() === market.targetMint.toString(),
);
// Find collateral token by matching mintKey
const collateralToken = ALL_TOKENS.find(
(token) => token.mintKey.toString() === market.collateralMint.toString(),
);
if (targetToken?.symbol && collateralToken?.symbol) {
marketSdkInfo[market.marketAccount.toString()] = {
tokenPair: `${targetToken.symbol}/${collateralToken.symbol}`,
token: targetToken.symbol,
side: Object.keys(market.side)[0],
pool: poolConfig.poolName,
};
}
});
});
export { marketSdkInfo };
export interface MarketTokenSides {
[token: string]: {
long?: { marketID: string };
short?: { marketID: string };
};
}
const marketTokenMap: MarketTokenSides = {};
// Convert marketSdkInfo into marketTokenMap
Object.entries(marketSdkInfo).forEach(([marketID, info]) => {
if (!marketTokenMap[info.token]) {
marketTokenMap[info.token] = {};
}
marketTokenMap[info.token][info.side.toLowerCase() as "long" | "short"] = {
marketID,
};
});
export { marketTokenMap };
interface TradingAccountResult {
nftReferralAccountPK: PublicKey | null;
nftTradingAccountPk: PublicKey | null;
nftOwnerRebateTokenAccountPk: PublicKey | null;
}
export async function getNftTradingAccountInfo(
userPublicKey: PublicKey,
perpClient: PerpetualsClient,
poolConfig: PoolConfig,
collateralCustodySymbol: string,
): Promise<TradingAccountResult> {
const getNFTReferralAccountPK = (publicKey: PublicKey) => {
return PublicKey.findProgramAddressSync(
[Buffer.from("referral"), publicKey.toBuffer()],
PROGRAM_ID,
)[0];
};
const nftReferralAccountPK = getNFTReferralAccountPK(userPublicKey);
const nftReferralAccountInfo =
await perpClient.provider.connection.getAccountInfo(nftReferralAccountPK);
let nftTradingAccountPk: PublicKey | null = null;
let nftOwnerRebateTokenAccountPk: PublicKey | null = null;
if (nftReferralAccountInfo) {
const nftReferralAccountData = perpClient.program.coder.accounts.decode(
"referral",
nftReferralAccountInfo.data,
) as Referral;
nftTradingAccountPk = nftReferralAccountData.refererTradingAccount;
if (nftTradingAccountPk) {
const nftTradingAccountInfo =
await perpClient.provider.connection.getAccountInfo(
nftTradingAccountPk,
);
if (nftTradingAccountInfo) {
const nftTradingAccount = perpClient.program.coder.accounts.decode(
"trading",
nftTradingAccountInfo.data,
) as { owner: PublicKey };
nftOwnerRebateTokenAccountPk = getAssociatedTokenAddressSync(
poolConfig.getTokenFromSymbol(collateralCustodySymbol).mintKey,
nftTradingAccount.owner,
);
// Check if the account exists
const accountExists =
await perpClient.provider.connection.getAccountInfo(
nftOwnerRebateTokenAccountPk,
);
if (!accountExists) {
console.error(
"NFT owner rebate token account does not exist and may need to be created",
);
}
}
}
}
return {
nftReferralAccountPK,
nftTradingAccountPk,
nftOwnerRebateTokenAccountPk,
};
}
/**
* Creates a new PerpetualsClient instance with the given connection and wallet
* @param connection Solana connection
* @param wallet Solana wallet
* @returns PerpetualsClient instance
*/
export function createPerpClient(
connection: Connection,
wallet: Keypair,
): PerpetualsClient {
const provider = new AnchorProvider(connection, new Wallet(wallet), {
commitment: "confirmed",
preflightCommitment: "confirmed",
skipPreflight: true,
});
return new PerpetualsClient(
provider,
POOL_CONFIGS[0].programId,
POOL_CONFIGS[0].perpComposibilityProgramId,
POOL_CONFIGS[0].fbNftRewardProgramId,
POOL_CONFIGS[0].rewardDistributionProgram.programId,
{},
);
}
export function get_flash_privilege(agent: SolanaAgentKit): Privilege {
const FLASH_PRIVILEGE = agent.config.FLASH_PRIVILEGE || "None";
switch (FLASH_PRIVILEGE.toLowerCase()) {
case "referral":
return Privilege.Referral;
case "nft":
return Privilege.NFT;
default:
return Privilege.None;
}
}