Merge branch 'main' into feature/totalbalance

This commit is contained in:
Michael Essiet
2025-01-07 09:12:32 +01:00
committed by GitHub
83 changed files with 32124 additions and 819 deletions

220
src/utils/AdrenaClient.ts Normal file
View 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
View 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,
{},
);
}