mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-06-02 23:26:50 +00:00
Add Flash.Trade leveraged open and close position
This commit is contained in:
@@ -59,6 +59,10 @@ import {
|
||||
fetchTokenReportSummary,
|
||||
fetchTokenDetailedReport,
|
||||
OrderParams,
|
||||
FlashTradeParams,
|
||||
FlashCloseTradeParams,
|
||||
flashOpenTrade,
|
||||
flashCloseTrade,
|
||||
} from "../tools";
|
||||
|
||||
import {
|
||||
@@ -537,4 +541,22 @@ export class SolanaAgentKit {
|
||||
async fetchTokenDetailedReport(mint: string): Promise<TokenCheck> {
|
||||
return fetchTokenDetailedReport(mint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a new trading position on Flash.Trade
|
||||
* @param params Flash trade parameters including market, side, collateral, leverage, and pool name
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
async flashOpenTrade(params: FlashTradeParams): Promise<string> {
|
||||
return flashOpenTrade(this, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes an existing trading position on Flash.Trade
|
||||
* @param params Flash trade close parameters
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
async flashCloseTrade(params: FlashCloseTradeParams): Promise<string> {
|
||||
return flashCloseTrade(this, params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,6 +774,131 @@ export class SolanaGetWalletAddressTool extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaFlashOpenTrade extends Tool {
|
||||
name = "solana_flash_open_trade";
|
||||
description = `Opens a new leveraged trading position on Flash.Trade exchange.
|
||||
|
||||
Inputs (input is a JSON string):
|
||||
token: string, one of ["SOL", "BTC", "ETH"] (required)
|
||||
side: string, either "long" or "short" (required)
|
||||
collateralUsd: number, amount in USD for collateral eg 10 (required)
|
||||
leverage: number, eg 5 for 5x leverage (required)
|
||||
|
||||
Example:
|
||||
{
|
||||
"token": "SOL",
|
||||
"side": "long",
|
||||
"collateralUsd": 10,
|
||||
"leverage": 5
|
||||
}`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const parsedInput = JSON.parse(input);
|
||||
|
||||
// Validate input parameters
|
||||
if (!parsedInput.token) {
|
||||
throw new Error("Token is required");
|
||||
}
|
||||
if (!["SOL", "BTC", "ETH"].includes(parsedInput.token)) {
|
||||
throw new Error('Token must be one of ["SOL", "BTC", "ETH"]');
|
||||
}
|
||||
if (!["long", "short"].includes(parsedInput.side)) {
|
||||
throw new Error('Side must be either "long" or "short"');
|
||||
}
|
||||
if (!parsedInput.collateralUsd || parsedInput.collateralUsd <= 0) {
|
||||
throw new Error("Collateral USD amount must be positive");
|
||||
}
|
||||
if (!parsedInput.leverage || parsedInput.leverage <= 0) {
|
||||
throw new Error("Leverage must be positive");
|
||||
}
|
||||
|
||||
const tx = await this.solanaKit.flashOpenTrade({
|
||||
token: parsedInput.token,
|
||||
side: parsedInput.side,
|
||||
collateralUsd: parsedInput.collateralUsd,
|
||||
leverage: parsedInput.leverage,
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: "Flash trade position opened successfully",
|
||||
transaction: tx,
|
||||
token: parsedInput.token,
|
||||
side: parsedInput.side,
|
||||
collateralUsd: parsedInput.collateralUsd,
|
||||
leverage: parsedInput.leverage,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaFlashCloseTrade extends Tool {
|
||||
name = "solana_flash_close_trade";
|
||||
description = `Closes an existing leveraged trading position on Flash.Trade exchange.
|
||||
|
||||
Inputs (input is a JSON string):
|
||||
token: string, one of ["SOL", "BTC", "ETH"] (required)
|
||||
side: string, either "long" or "short" (required)
|
||||
|
||||
Example:
|
||||
{
|
||||
"token": "SOL",
|
||||
"side": "long"
|
||||
}`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const parsedInput = JSON.parse(input);
|
||||
|
||||
// Validate input parameters
|
||||
if (!parsedInput.token) {
|
||||
throw new Error("Token is required");
|
||||
}
|
||||
if (!["SOL", "BTC", "ETH"].includes(parsedInput.token)) {
|
||||
throw new Error('Token must be one of ["SOL", "BTC", "ETH"]');
|
||||
}
|
||||
if (!["long", "short"].includes(parsedInput.side)) {
|
||||
throw new Error('Side must be either "long" or "short"');
|
||||
}
|
||||
|
||||
const tx = await this.solanaKit.flashCloseTrade({
|
||||
token: parsedInput.token,
|
||||
side: parsedInput.side,
|
||||
});
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: "Flash trade position closed successfully",
|
||||
transaction: tx,
|
||||
token: parsedInput.token,
|
||||
side: parsedInput.side,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SolanaPumpfunTokenLaunchTool extends Tool {
|
||||
name = "solana_launch_pumpfun_token";
|
||||
|
||||
@@ -2175,5 +2300,8 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
|
||||
new SolanaFetchTokenDetailedReportTool(solanaKit),
|
||||
new SolanaPerpOpenTradeTool(solanaKit),
|
||||
new SolanaPerpCloseTradeTool(solanaKit),
|
||||
new SolanaFlashOpenTrade(solanaKit),
|
||||
new SolanaFlashCloseTrade(solanaKit),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
124
src/tools/flash_close_trade.ts
Normal file
124
src/tools/flash_close_trade.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { PublicKey, ComputeBudgetProgram } from "@solana/web3.js";
|
||||
import {
|
||||
PerpetualsClient,
|
||||
OraclePrice,
|
||||
PoolConfig,
|
||||
Privilege,
|
||||
Side,
|
||||
} from "flash-sdk";
|
||||
import { BN } from "@coral-xyz/anchor";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import {
|
||||
CLOSE_POSITION_CU,
|
||||
marketSdkInfo,
|
||||
marketTokenMap,
|
||||
getNftTradingAccountInfo,
|
||||
fetchOraclePrice,
|
||||
createPerpClient,
|
||||
} from "../utils/flashUtils";
|
||||
|
||||
export interface FlashCloseTradeParams {
|
||||
token: string;
|
||||
side: "long" | "short";
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes an existing position on Flash.Trade
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param params Trade parameters
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
export async function flashCloseTrade(
|
||||
agent: SolanaAgentKit,
|
||||
params: FlashCloseTradeParams,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const { token, side } = params;
|
||||
|
||||
// Get market ID from token and side using marketTokenMap
|
||||
const tokenMarkets = marketTokenMap[token];
|
||||
if (!tokenMarkets) {
|
||||
throw new Error(`Token ${token} not supported for trading`);
|
||||
}
|
||||
|
||||
const sideEntry = tokenMarkets[side];
|
||||
if (!sideEntry) {
|
||||
throw new Error(`${side} side not available for ${token}`);
|
||||
}
|
||||
|
||||
const market = sideEntry.marketID;
|
||||
|
||||
// Validate market data using marketSdkInfo
|
||||
const marketData = marketSdkInfo[market];
|
||||
if (!marketData) {
|
||||
throw new Error(`Invalid market configuration for ${token}/${side}`);
|
||||
}
|
||||
|
||||
// Get token information
|
||||
const [targetSymbol, collateralSymbol] = marketData.tokenPair.split("/");
|
||||
|
||||
// Fetch oracle prices
|
||||
const [targetPrice, collateralPrice] = await Promise.all([
|
||||
fetchOraclePrice(targetSymbol),
|
||||
fetchOraclePrice(collateralSymbol),
|
||||
]);
|
||||
|
||||
// Initialize pool configuration and perpClient
|
||||
const poolConfig = PoolConfig.fromIdsByName(marketData.pool, "mainnet-beta");
|
||||
const perpClient = createPerpClient(agent.connection, agent.wallet);
|
||||
|
||||
// Calculate price after slippage
|
||||
const slippageBpsBN = new BN(100); // 1% slippage
|
||||
const sideEnum = side === "long" ? Side.Long : Side.Short;
|
||||
const priceWithSlippage = perpClient.getPriceAfterSlippage(
|
||||
false, // isEntry = false for closing position
|
||||
slippageBpsBN,
|
||||
targetPrice.price,
|
||||
sideEnum,
|
||||
);
|
||||
|
||||
// Get NFT trading account info
|
||||
const tradingAccounts = await getNftTradingAccountInfo(
|
||||
agent.wallet_address,
|
||||
perpClient,
|
||||
poolConfig,
|
||||
collateralSymbol,
|
||||
);
|
||||
|
||||
if (
|
||||
!tradingAccounts.nftTradingAccountPk ||
|
||||
!tradingAccounts.nftReferralAccountPK ||
|
||||
!tradingAccounts.nftOwnerRebateTokenAccountPk
|
||||
) {
|
||||
throw new Error("Required NFT trading accounts not found");
|
||||
}
|
||||
|
||||
// Build and send transaction
|
||||
const { instructions, additionalSigners } = await perpClient.closePosition(
|
||||
targetSymbol,
|
||||
collateralSymbol,
|
||||
priceWithSlippage,
|
||||
sideEnum,
|
||||
poolConfig,
|
||||
Privilege.Referral,
|
||||
tradingAccounts.nftTradingAccountPk,
|
||||
tradingAccounts.nftReferralAccountPK,
|
||||
tradingAccounts.nftOwnerRebateTokenAccountPk,
|
||||
);
|
||||
|
||||
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: CLOSE_POSITION_CU,
|
||||
});
|
||||
|
||||
return await perpClient.sendTransaction(
|
||||
[computeBudgetIx, ...instructions],
|
||||
{
|
||||
additionalSigners: additionalSigners,
|
||||
alts: perpClient.addressLookupTables,
|
||||
prioritizationFee: 5000000,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Flash trade close failed: ${error}`);
|
||||
}
|
||||
}
|
||||
254
src/tools/flash_open_trade.ts
Normal file
254
src/tools/flash_open_trade.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { PublicKey, ComputeBudgetProgram } from "@solana/web3.js";
|
||||
import {
|
||||
PerpetualsClient,
|
||||
OraclePrice,
|
||||
PoolConfig,
|
||||
Privilege,
|
||||
Side,
|
||||
CustodyAccount,
|
||||
Custody,
|
||||
} from "flash-sdk";
|
||||
import { BN } from "@coral-xyz/anchor";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import {
|
||||
ALL_TOKENS,
|
||||
marketSdkInfo,
|
||||
marketTokenMap,
|
||||
getNftTradingAccountInfo,
|
||||
OPEN_POSITION_CU,
|
||||
fetchOraclePrice,
|
||||
createPerpClient,
|
||||
} from "../utils/flashUtils";
|
||||
|
||||
export interface FlashTradeParams {
|
||||
token: string;
|
||||
side: "long" | "short";
|
||||
collateralUsd: number;
|
||||
leverage: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a new position on Flash.Trade
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param params Trade parameters
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
export async function flashOpenTrade(
|
||||
agent: SolanaAgentKit,
|
||||
params: FlashTradeParams,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const { token, side, collateralUsd, leverage } = params;
|
||||
|
||||
// Get market ID from token and side using marketTokenMap
|
||||
const tokenMarkets = marketTokenMap[token];
|
||||
if (!tokenMarkets) {
|
||||
throw new Error(`Token ${token} not supported for trading`);
|
||||
}
|
||||
|
||||
const sideEntry = tokenMarkets[side];
|
||||
if (!sideEntry) {
|
||||
throw new Error(`${side} side not available for ${token}`);
|
||||
}
|
||||
|
||||
const market = sideEntry.marketID;
|
||||
|
||||
// Validate market data using marketSdkInfo
|
||||
const marketData = marketSdkInfo[market];
|
||||
if (!marketData) {
|
||||
throw new Error(`Invalid market configuration for ${token}/${side}`);
|
||||
}
|
||||
|
||||
// Get token information
|
||||
const [targetSymbol, collateralSymbol] = marketData.tokenPair.split("/");
|
||||
const targetToken = ALL_TOKENS.find((t) => t.symbol === targetSymbol);
|
||||
const collateralToken = ALL_TOKENS.find(
|
||||
(t) => t.symbol === collateralSymbol,
|
||||
);
|
||||
|
||||
if (!targetToken || !collateralToken) {
|
||||
throw new Error(`Token not found for pair ${marketData.tokenPair}`);
|
||||
}
|
||||
|
||||
// Fetch oracle prices
|
||||
const [targetPrice, collateralPrice] = await Promise.all([
|
||||
fetchOraclePrice(targetSymbol),
|
||||
fetchOraclePrice(collateralSymbol),
|
||||
]);
|
||||
|
||||
// Initialize pool configuration and perpClient
|
||||
const poolConfig = PoolConfig.fromIdsByName(marketData.pool, "mainnet-beta");
|
||||
const perpClient = createPerpClient(agent.connection, agent.wallet);
|
||||
|
||||
// Calculate position parameters
|
||||
const leverageBN = new BN(leverage);
|
||||
const collateralTokenPrice = convertPriceToNumber(collateralPrice.price);
|
||||
const collateralAmount = calculateCollateralAmount(
|
||||
collateralUsd,
|
||||
collateralTokenPrice,
|
||||
collateralToken.decimals,
|
||||
);
|
||||
|
||||
// Get custody accounts
|
||||
const { targetCustody, collateralCustody } = await fetchCustodyAccounts(
|
||||
perpClient,
|
||||
poolConfig,
|
||||
targetSymbol,
|
||||
collateralSymbol,
|
||||
);
|
||||
|
||||
// Calculate position size
|
||||
const positionSize = calculatePositionSize(
|
||||
perpClient,
|
||||
collateralAmount,
|
||||
leverageBN,
|
||||
targetToken,
|
||||
collateralToken,
|
||||
side,
|
||||
targetPrice.price,
|
||||
collateralPrice.price,
|
||||
targetCustody,
|
||||
collateralCustody,
|
||||
);
|
||||
|
||||
// Get NFT trading account info
|
||||
const tradingAccounts = await getNftTradingAccountInfo(
|
||||
agent.wallet_address,
|
||||
perpClient,
|
||||
poolConfig,
|
||||
collateralSymbol,
|
||||
);
|
||||
|
||||
if (
|
||||
!tradingAccounts.nftTradingAccountPk ||
|
||||
!tradingAccounts.nftReferralAccountPK
|
||||
) {
|
||||
throw new Error("Required NFT trading accounts not found");
|
||||
}
|
||||
|
||||
// Prepare transaction
|
||||
const slippageBps = new BN(1000);
|
||||
const priceWithSlippage = perpClient.getPriceAfterSlippage(
|
||||
true,
|
||||
slippageBps,
|
||||
targetPrice.price,
|
||||
side === "long" ? Side.Long : Side.Short,
|
||||
);
|
||||
|
||||
// Build and send transaction
|
||||
const { instructions, additionalSigners } = await perpClient.openPosition(
|
||||
targetSymbol,
|
||||
collateralSymbol,
|
||||
priceWithSlippage,
|
||||
collateralAmount,
|
||||
positionSize,
|
||||
side === "long" ? Side.Long : Side.Short,
|
||||
poolConfig,
|
||||
Privilege.Referral,
|
||||
tradingAccounts.nftTradingAccountPk,
|
||||
tradingAccounts.nftReferralAccountPK,
|
||||
tradingAccounts.nftOwnerRebateTokenAccountPk!,
|
||||
false,
|
||||
);
|
||||
|
||||
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: OPEN_POSITION_CU,
|
||||
});
|
||||
|
||||
return await perpClient.sendTransaction(
|
||||
[computeBudgetIx, ...instructions],
|
||||
{
|
||||
additionalSigners: additionalSigners,
|
||||
alts: perpClient.addressLookupTables,
|
||||
prioritizationFee: 5000000,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Flash trade failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function convertPriceToNumber(oraclePrice: OraclePrice): number {
|
||||
const price = parseInt(oraclePrice.price.toString("hex"), 16);
|
||||
const exponent = parseInt(oraclePrice.exponent.toString("hex"), 16);
|
||||
return price * Math.pow(10, exponent);
|
||||
}
|
||||
|
||||
function calculateCollateralAmount(
|
||||
usdAmount: number,
|
||||
tokenPrice: number,
|
||||
decimals: number,
|
||||
): BN {
|
||||
return new BN((usdAmount / tokenPrice) * Math.pow(10, decimals));
|
||||
}
|
||||
|
||||
async function fetchCustodyAccounts(
|
||||
perpClient: PerpetualsClient,
|
||||
poolConfig: PoolConfig,
|
||||
targetSymbol: string,
|
||||
collateralSymbol: string,
|
||||
) {
|
||||
const targetConfig = poolConfig.custodies.find(
|
||||
(c) => c.symbol === targetSymbol,
|
||||
);
|
||||
const collateralConfig = poolConfig.custodies.find(
|
||||
(c) => c.symbol === collateralSymbol,
|
||||
);
|
||||
|
||||
if (!targetConfig || !collateralConfig) {
|
||||
throw new Error("Custody configuration not found");
|
||||
}
|
||||
|
||||
const accounts = await perpClient.provider.connection.getMultipleAccountsInfo(
|
||||
[targetConfig.custodyAccount, collateralConfig.custodyAccount],
|
||||
);
|
||||
|
||||
if (!accounts[0] || !accounts[1]) {
|
||||
throw new Error("Failed to fetch custody accounts");
|
||||
}
|
||||
|
||||
return {
|
||||
targetCustody: CustodyAccount.from(
|
||||
targetConfig.custodyAccount,
|
||||
perpClient.program.coder.accounts.decode<Custody>(
|
||||
"custody",
|
||||
accounts[0].data,
|
||||
),
|
||||
),
|
||||
collateralCustody: CustodyAccount.from(
|
||||
collateralConfig.custodyAccount,
|
||||
perpClient.program.coder.accounts.decode<Custody>(
|
||||
"custody",
|
||||
accounts[1].data,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function calculatePositionSize(
|
||||
perpClient: PerpetualsClient,
|
||||
collateralAmount: BN,
|
||||
leverage: BN,
|
||||
targetToken: any,
|
||||
collateralToken: any,
|
||||
side: "long" | "short",
|
||||
targetPrice: OraclePrice,
|
||||
collateralPrice: OraclePrice,
|
||||
targetCustody: CustodyAccount,
|
||||
collateralCustody: CustodyAccount,
|
||||
): BN {
|
||||
return perpClient.getSizeAmountFromLeverageAndCollateral(
|
||||
collateralAmount,
|
||||
leverage.toString(),
|
||||
targetToken,
|
||||
collateralToken,
|
||||
side === "long" ? Side.Long : Side.Short,
|
||||
targetPrice,
|
||||
targetPrice,
|
||||
targetCustody,
|
||||
collateralPrice,
|
||||
collateralPrice,
|
||||
collateralCustody,
|
||||
);
|
||||
}
|
||||
@@ -59,3 +59,6 @@ export * from "./create_tiplinks";
|
||||
|
||||
export * from "./tensor_trade";
|
||||
export * from "./rugcheck";
|
||||
|
||||
export * from "./flash_open_trade";
|
||||
export * from "./flash_close_trade";
|
||||
310
src/utils/flashUtils.ts
Normal file
310
src/utils/flashUtils.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
|
||||
import { OraclePrice } from "flash-sdk";
|
||||
import { AnchorProvider, BN, Wallet } from "@coral-xyz/anchor";
|
||||
import { PoolConfig, Token, Referral, PerpetualsClient } from "flash-sdk";
|
||||
import { Cluster, PublicKey, Connection, Keypair } from "@solana/web3.js";
|
||||
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
|
||||
|
||||
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 priceServiceConnection = new PriceServiceConnection(HERMES_URL, {
|
||||
priceFeedRequestConfig: {
|
||||
binary: true,
|
||||
},
|
||||
});
|
||||
|
||||
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 priceFeed = await priceServiceConnection.getLatestPriceFeeds([
|
||||
priceFeedId,
|
||||
]);
|
||||
|
||||
if (!priceFeed || priceFeed.length === 0) {
|
||||
throw new Error(`No price feed received for ${symbol}`);
|
||||
}
|
||||
|
||||
const price = priceFeed[0].getPriceUnchecked();
|
||||
const emaPrice = priceFeed[0].getEmaPriceUnchecked();
|
||||
|
||||
const priceOracle = new OraclePrice({
|
||||
price: new BN(price.price),
|
||||
exponent: new BN(price.expo),
|
||||
confidence: new BN(price.conf),
|
||||
timestamp: new BN(price.publishTime),
|
||||
});
|
||||
|
||||
const emaPriceOracle = new OraclePrice({
|
||||
price: new BN(emaPrice.price),
|
||||
exponent: new BN(emaPrice.expo),
|
||||
confidence: new BN(emaPrice.conf),
|
||||
timestamp: new BN(emaPrice.publishTime),
|
||||
});
|
||||
|
||||
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: priceOracle,
|
||||
emaPrice: emaPriceOracle,
|
||||
isStale: false,
|
||||
status: status,
|
||||
};
|
||||
|
||||
return pythPriceEntry;
|
||||
} catch (error) {
|
||||
console.error(`Error in fetchOraclePrice for ${symbol}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// If you need to get all price IDs for subscription or other purposes
|
||||
export const getAllPriceIds = () => ALL_TOKENS.map((t) => t.pythPriceId);
|
||||
|
||||
export const subscribeToPriceFeeds = (
|
||||
callback: (symbol: string, priceEntry: PythPriceEntry) => void,
|
||||
) => {
|
||||
const priceIds = getAllPriceIds();
|
||||
priceServiceConnection.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
|
||||
const token = ALL_TOKENS.find((f) => f.pythPriceId === `0x${priceFeed.id}`);
|
||||
if (token) {
|
||||
const priceOracle = new OraclePrice({
|
||||
price: new BN(priceFeed.getPriceUnchecked().price),
|
||||
exponent: new BN(priceFeed.getPriceUnchecked().expo),
|
||||
confidence: new BN(priceFeed.getPriceUnchecked().conf),
|
||||
timestamp: new BN(priceFeed.getPriceUnchecked().publishTime),
|
||||
});
|
||||
const emaPriceOracle = new OraclePrice({
|
||||
price: new BN(priceFeed.getEmaPriceUnchecked().price),
|
||||
exponent: new BN(priceFeed.getEmaPriceUnchecked().expo),
|
||||
confidence: new BN(priceFeed.getEmaPriceUnchecked().conf),
|
||||
timestamp: new BN(priceFeed.getEmaPriceUnchecked().publishTime),
|
||||
});
|
||||
|
||||
const status = !token.isVirtual
|
||||
? PriceStatus.Trading
|
||||
: PriceStatus.Unknown;
|
||||
const priceEntry: PythPriceEntry = {
|
||||
price: priceOracle,
|
||||
emaPrice: emaPriceOracle,
|
||||
isStale: false,
|
||||
status: status,
|
||||
};
|
||||
callback(token.symbol, priceEntry);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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.log(
|
||||
"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,
|
||||
{}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user