mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-22 23:26:45 +00:00
Merge branch 'main' into feature/totalbalance
This commit is contained in:
220
src/utils/AdrenaClient.ts
Normal file
220
src/utils/AdrenaClient.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import { AnchorProvider, IdlAccounts, Program } from "@coral-xyz/anchor";
|
||||
import { Adrena, IDL as ADRENA_IDL } from "../idls/adrena";
|
||||
|
||||
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { TOKENS } from "../constants";
|
||||
|
||||
export type AdrenaProgram = Program<Adrena>;
|
||||
|
||||
type Accounts = IdlAccounts<Adrena>;
|
||||
|
||||
export type Cortex = Accounts["cortex"];
|
||||
export type Custody = Accounts["custody"] & { pubkey: PublicKey };
|
||||
export type Pool = Accounts["pool"];
|
||||
|
||||
export default class AdrenaClient {
|
||||
public static programId = new PublicKey(
|
||||
"13gDzEXCdocbj8iAiqrScGo47NiSuYENGsRqi3SEAwet",
|
||||
);
|
||||
|
||||
constructor(
|
||||
public program: AdrenaProgram,
|
||||
public mainPool: Pool,
|
||||
public cortex: Cortex,
|
||||
public custodies: Custody[],
|
||||
) {}
|
||||
|
||||
public static mainPool = new PublicKey(
|
||||
"4bQRutgDJs6vuh6ZcWaPVXiQaBzbHketjbCDjL4oRN34",
|
||||
);
|
||||
|
||||
public static async load(agent: SolanaAgentKit): Promise<AdrenaClient> {
|
||||
const program = new Program<Adrena>(
|
||||
ADRENA_IDL,
|
||||
AdrenaClient.programId,
|
||||
new AnchorProvider(agent.connection, new NodeWallet(agent.wallet), {
|
||||
commitment: "processed",
|
||||
skipPreflight: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const [cortex, mainPool] = await Promise.all([
|
||||
program.account.cortex.fetch(AdrenaClient.cortex),
|
||||
program.account.pool.fetch(AdrenaClient.mainPool),
|
||||
]);
|
||||
|
||||
const custodiesAddresses = mainPool.custodies.filter(
|
||||
(custody) => !custody.equals(PublicKey.default),
|
||||
);
|
||||
|
||||
const custodies =
|
||||
await program.account.custody.fetchMultiple(custodiesAddresses);
|
||||
|
||||
if (!custodies.length || custodies.some((c) => c === null)) {
|
||||
throw new Error("Custodies not found");
|
||||
}
|
||||
|
||||
return new AdrenaClient(
|
||||
program,
|
||||
mainPool,
|
||||
cortex,
|
||||
(custodies as Custody[]).map((c, i) => ({
|
||||
...c,
|
||||
pubkey: custodiesAddresses[i],
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
public static findCustodyAddress(mint: PublicKey): PublicKey {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[
|
||||
Buffer.from("custody"),
|
||||
AdrenaClient.mainPool.toBuffer(),
|
||||
mint.toBuffer(),
|
||||
],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
}
|
||||
|
||||
public static findCustodyTokenAccountAddress(mint: PublicKey) {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[
|
||||
Buffer.from("custody_token_account"),
|
||||
AdrenaClient.mainPool.toBuffer(),
|
||||
mint.toBuffer(),
|
||||
],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
}
|
||||
|
||||
public static findPositionAddress(
|
||||
owner: PublicKey,
|
||||
custody: PublicKey,
|
||||
side: "long" | "short",
|
||||
) {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[
|
||||
Buffer.from("position"),
|
||||
owner.toBuffer(),
|
||||
AdrenaClient.mainPool.toBuffer(),
|
||||
custody.toBuffer(),
|
||||
Buffer.from([
|
||||
{
|
||||
long: 1,
|
||||
short: 2,
|
||||
}[side],
|
||||
]),
|
||||
],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
}
|
||||
|
||||
public static cortex = PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("cortex")],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
|
||||
public static lpTokenMint = PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("lp_token_mint"), AdrenaClient.mainPool.toBuffer()],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
|
||||
public static lmTokenMint = PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("lm_token_mint")],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
|
||||
public static getStakingPda(stakedTokenMint: PublicKey) {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("staking"), stakedTokenMint.toBuffer()],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
}
|
||||
|
||||
public static lmStaking = AdrenaClient.getStakingPda(
|
||||
AdrenaClient.lmTokenMint,
|
||||
);
|
||||
|
||||
public static lpStaking = AdrenaClient.getStakingPda(
|
||||
AdrenaClient.lpTokenMint,
|
||||
);
|
||||
|
||||
public static transferAuthority = PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("transfer_authority")],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
|
||||
public static findATAAddressSync(
|
||||
wallet: PublicKey,
|
||||
mint: PublicKey,
|
||||
): PublicKey {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
)[0];
|
||||
}
|
||||
|
||||
public getCustodyByMint(mint: PublicKey): Custody {
|
||||
const custody = this.custodies.find((custody) => custody.mint.equals(mint));
|
||||
|
||||
if (!custody) {
|
||||
throw new Error(`Cannot find custody for mint ${mint.toBase58()}`);
|
||||
}
|
||||
|
||||
return custody;
|
||||
}
|
||||
|
||||
public static getUserProfilePda(wallet: PublicKey) {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("user_profile"), wallet.toBuffer()],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
}
|
||||
|
||||
public static stakingRewardTokenMint = TOKENS.USDC;
|
||||
|
||||
public static getStakingRewardTokenVaultPda(stakingPda: PublicKey) {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("staking_reward_token_vault"), stakingPda.toBuffer()],
|
||||
AdrenaClient.programId,
|
||||
)[0];
|
||||
}
|
||||
|
||||
public static lmStakingRewardTokenVault =
|
||||
AdrenaClient.getStakingRewardTokenVaultPda(AdrenaClient.lmStaking);
|
||||
public static lpStakingRewardTokenVault =
|
||||
AdrenaClient.getStakingRewardTokenVaultPda(AdrenaClient.lpStaking);
|
||||
|
||||
public static async isAccountInitialized(
|
||||
connection: Connection,
|
||||
address: PublicKey,
|
||||
): Promise<boolean> {
|
||||
return !!(await connection.getAccountInfo(address));
|
||||
}
|
||||
|
||||
public static createATAInstruction({
|
||||
ataAddress,
|
||||
mint,
|
||||
owner,
|
||||
payer = owner,
|
||||
}: {
|
||||
ataAddress: PublicKey;
|
||||
mint: PublicKey;
|
||||
owner: PublicKey;
|
||||
payer?: PublicKey;
|
||||
}) {
|
||||
return createAssociatedTokenAccountInstruction(
|
||||
payer,
|
||||
ataAddress,
|
||||
owner,
|
||||
mint,
|
||||
);
|
||||
}
|
||||
}
|
||||
280
src/utils/flashUtils.ts
Normal file
280
src/utils/flashUtils.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { HermesClient } from "@pythnetwork/hermes-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 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,
|
||||
{},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user