Add Adrena Protocol Open/Close PERP trade support

This commit is contained in:
Orex
2025-01-01 14:57:55 +04:00
parent 61abe10b0d
commit a68f26d009
8 changed files with 21577 additions and 0 deletions

View File

@@ -54,6 +54,7 @@ Anyone - whether an SF-based AI researcher or a crypto-native builder - can brin
- Jito Bundles
- Pyth Price feeds for fetching Asset Prices
- Register/resolve Alldomains
- Perpetuals Trading with Adrena Protocol
- **Solana Blinks**
- Lending by Lulo (Best APR for USDC)
@@ -220,6 +221,32 @@ const price = await agent.pythFetchPrice(
console.log("Price in BTC/USD:", price);
```
### Open PERP Trade
```typescript
import { PublicKey } from "@solana/web3.js";
const signature = await agent.openPerpTradeLong({
price: 300, // $300 SOL Max price
collateralAmount: 10, // 10 jitoSOL in
collateralMint: new PublicKey("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"), // jitoSOL
leverage: 50000, // x5
tradeMint: new PublicKey("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"), // jitoSOL
slippage: 0.3, // 0.3%
});
```
### Close PERP Trade
```typescript
import { PublicKey } from "@solana/web3.js";
const signature = await agent.closePerpTradeLong({
price: 200, // $200 SOL price
tradeMint: new PublicKey("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"), // jitoSOL
});
```
## Examples
### LangGraph Multi-Agent System

View File

@@ -26,6 +26,10 @@ import {
batchOrder,
cancelAllOrders,
withdrawAll,
closePerpTradeShort,
closePerpTradeLong,
openPerpTradeShort,
openPerpTradeLong,
transfer,
getTokenDataByAddress,
getTokenDataByTicker,
@@ -210,6 +214,42 @@ export class SolanaAgentKit {
return withdrawAll(this, marketId);
}
async openPerpTradeLong(
args: Omit<Parameters<typeof openPerpTradeLong>[0], "agent">,
): Promise<string> {
return openPerpTradeLong({
agent: this,
...args,
});
}
async openPerpTradeShort(
args: Omit<Parameters<typeof openPerpTradeShort>[0], "agent">,
): Promise<string> {
return openPerpTradeShort({
agent: this,
...args,
});
}
async closePerpTradeShort(
args: Omit<Parameters<typeof closePerpTradeShort>[0], "agent">,
): Promise<string> {
return closePerpTradeShort({
agent: this,
...args,
});
}
async closePerpTradeLong(
args: Omit<Parameters<typeof closePerpTradeLong>[0], "agent">,
): Promise<string> {
return closePerpTradeLong({
agent: this,
...args,
});
}
async lendAssets(amount: number): Promise<string> {
return lendAsset(this, amount);
}

View File

@@ -18,11 +18,13 @@ export const TOKENS = {
* Default configuration options
* @property {number} SLIPPAGE_BPS - Default slippage tolerance in basis points (300 = 3%)
* @property {number} TOKEN_DECIMALS - Default number of decimals for new tokens
* @property {number} LEVERAGE_BPS - Default leverage for trading PERP
*/
export const DEFAULT_OPTIONS = {
SLIPPAGE_BPS: 300,
TOKEN_DECIMALS: 9,
RERERRAL_FEE: 200,
LEVERAGE_BPS: 50000, // 10000 = x1, 50000 = x5, 100000 = x10, 1000000 = x100
} as const;
/**

20671
src/idls/adrena.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -261,6 +261,114 @@ export class SolanaMintNFTTool extends Tool {
}
}
export class SolanaPerpCloseTradeTool extends Tool {
name = "solana_close_perp_trade";
description = `This tool can be used to close perpetuals trade ( It uses Adrena Protocol ).
Inputs ( input is a JSON string ):
tradeMint: string, eg "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" etc. (optional)
price?: number, eg 100 (optional)
side: string, eg: "long" or "short"`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = JSON.parse(input);
const tx =
parsedInput.side === "long"
? await this.solanaKit.closePerpTradeLong({
price: parsedInput.price,
tradeMint: new PublicKey(parsedInput.tradeMint),
})
: await this.solanaKit.closePerpTradeShort({
price: parsedInput.price,
tradeMint: new PublicKey(parsedInput.tradeMint),
});
return JSON.stringify({
status: "success",
message: "Perpetual trade closed successfully",
transaction: tx,
price: parsedInput.price,
tradeMint: new PublicKey(parsedInput.tradeMint),
side: parsedInput.side,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaPerpOpenTradeTool extends Tool {
name = "solana_open_perp_trade";
description = `This tool can be used to open perpetuals trade ( It uses Adrena Protocol ).
Inputs ( input is a JSON string ):
collateralAmount: number, eg 1 or 0.01 (required)
collateralMint: string, eg "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn" or "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" etc. (optional)
tradeMint: string, eg "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" etc. (optional)
leverage: number, eg 50000 = x5, 100000 = x10, 1000000 = x100 (optional)
price?: number, eg 100 (optional)
slippage?: number, eg 0.3 (optional)
side: string, eg: "long" or "short"`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = JSON.parse(input);
const tx =
parsedInput.side === "long"
? await this.solanaKit.openPerpTradeLong({
price: parsedInput.price,
collateralAmount: parsedInput.collateralAmount,
collateralMint: new PublicKey(parsedInput.collateralMint),
leverage: parsedInput.leverage,
tradeMint: new PublicKey(parsedInput.tradeMint),
slippage: parsedInput.slippage,
})
: await this.solanaKit.openPerpTradeLong({
price: parsedInput.price,
collateralAmount: parsedInput.collateralAmount,
collateralMint: new PublicKey(parsedInput.collateralMint),
leverage: parsedInput.leverage,
tradeMint: new PublicKey(parsedInput.tradeMint),
slippage: parsedInput.slippage,
});
return JSON.stringify({
status: "success",
message: "Perpetual trade opened successfully",
transaction: tx,
price: parsedInput.price,
collateralAmount: parsedInput.collateralAmount,
collateralMint: new PublicKey(parsedInput.collateralMint),
leverage: parsedInput.leverage,
tradeMint: new PublicKey(parsedInput.tradeMint),
slippage: parsedInput.slippage,
side: parsedInput.side,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaTradeTool extends Tool {
name = "solana_trade";
description = `This tool can be used to swap tokens to another token ( It uses Jupiter Exchange ).
@@ -2065,5 +2173,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
new SolanaCancelNFTListingTool(solanaKit),
new SolanaFetchTokenReportSummaryTool(solanaKit),
new SolanaFetchTokenDetailedReportTool(solanaKit),
new SolanaPerpOpenTradeTool(solanaKit),
new SolanaPerpCloseTradeTool(solanaKit),
];
}

View File

@@ -0,0 +1,506 @@
import {
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
import { TOKENS, DEFAULT_OPTIONS } from "../constants";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { BN } from "@coral-xyz/anchor";
import AdrenaClient from "../utils/AdrenaClient";
import { sendTx } from "../utils/send_tx";
const PRICE_DECIMALS = 10;
const ADRENA_PROGRAM_ID = new PublicKey(
"13gDzEXCdocbj8iAiqrScGo47NiSuYENGsRqi3SEAwet",
);
// i.e percentage = -2 (for -2%)
// i.e percentage = 5 (for 5%)
function applySlippage(nb: BN, percentage: number): BN {
const negative = percentage < 0 ? true : false;
// Do x10_000 so percentage can be up to 4 decimals
const percentageBN = new BN(
(negative ? percentage * -1 : percentage) * 10_000,
);
const delta = nb.mul(percentageBN).divRound(new BN(10_000 * 100));
return negative ? nb.sub(delta) : nb.add(delta);
}
/**
* Close short trade on Adrena
* @returns Transaction signature
*/
export async function closePerpTradeShort({
agent,
price,
tradeMint,
}: {
agent: SolanaAgentKit;
price: number;
tradeMint: PublicKey;
}) {
const client = await AdrenaClient.load(agent);
const owner = agent.wallet.publicKey;
const custody = client.getCustodyByMint(tradeMint);
const collateralCustody = client.getCustodyByMint(TOKENS.USDC);
const stakingRewardTokenCustodyAccount = client.getCustodyByMint(
AdrenaClient.stakingRewardTokenMint,
);
const stakingRewardTokenCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(
AdrenaClient.stakingRewardTokenMint,
);
const position = AdrenaClient.findPositionAddress(
owner,
custody.pubkey,
"long",
);
const userProfilePda = AdrenaClient.getUserProfilePda(owner);
const userProfile =
await client.program.account.userProfile.fetchNullable(userProfilePda);
const receivingAccount = AdrenaClient.findATAAddressSync(
owner,
collateralCustody.mint,
);
const preInstructions: TransactionInstruction[] = [];
const collateralCustodyOracle = collateralCustody.oracle;
const collateralCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(collateralCustody.mint);
if (
!(await AdrenaClient.isAccountInitialized(
agent.connection,
receivingAccount,
))
) {
preInstructions.push(
AdrenaClient.createATAInstruction({
ataAddress: receivingAccount,
mint: collateralCustody.mint,
owner,
}),
);
}
const instruction = await client.program.methods
.closePositionShort({
price: new BN(price * 10 ** PRICE_DECIMALS),
})
.accountsStrict({
owner,
receivingAccount,
transferAuthority: AdrenaClient.transferAuthority,
pool: AdrenaClient.mainPool,
position: position,
custody: custody.pubkey,
custodyTradeOracle: custody.tradeOracle,
tokenProgram: TOKEN_PROGRAM_ID,
lmStaking: AdrenaClient.lmStaking,
lpStaking: AdrenaClient.lpStaking,
cortex: AdrenaClient.cortex,
stakingRewardTokenCustody: stakingRewardTokenCustodyAccount.pubkey,
stakingRewardTokenCustodyOracle: stakingRewardTokenCustodyAccount.oracle,
stakingRewardTokenCustodyTokenAccount,
lmStakingRewardTokenVault: AdrenaClient.lmStakingRewardTokenVault,
lpStakingRewardTokenVault: AdrenaClient.lpStakingRewardTokenVault,
lpTokenMint: AdrenaClient.lpTokenMint,
protocolFeeRecipient: client.cortex.protocolFeeRecipient,
adrenaProgram: AdrenaClient.programId,
userProfile: userProfile ? userProfilePda : null,
caller: owner,
collateralCustody: collateralCustody.pubkey,
collateralCustodyOracle,
collateralCustodyTokenAccount,
})
.instruction();
return sendTx(agent, [...preInstructions, instruction]);
}
/**
* Close long trade on Adrena
* @returns Transaction signature
*/
export async function closePerpTradeLong({
agent,
price,
tradeMint,
}: {
agent: SolanaAgentKit;
price: number;
tradeMint: PublicKey;
}) {
const client = await AdrenaClient.load(agent);
const owner = agent.wallet.publicKey;
const custody = client.getCustodyByMint(tradeMint);
const custodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(tradeMint);
const stakingRewardTokenCustodyAccount = client.getCustodyByMint(
AdrenaClient.stakingRewardTokenMint,
);
const stakingRewardTokenCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(
AdrenaClient.stakingRewardTokenMint,
);
const position = AdrenaClient.findPositionAddress(
owner,
custody.pubkey,
"long",
);
const userProfilePda = AdrenaClient.getUserProfilePda(owner);
const userProfile =
await client.program.account.userProfile.fetchNullable(userProfilePda);
const receivingAccount = AdrenaClient.findATAAddressSync(owner, custody.mint);
const preInstructions: TransactionInstruction[] = [];
if (
!(await AdrenaClient.isAccountInitialized(
agent.connection,
receivingAccount,
))
) {
preInstructions.push(
AdrenaClient.createATAInstruction({
ataAddress: receivingAccount,
mint: custody.mint,
owner,
}),
);
}
const instruction = await client.program.methods
.closePositionLong({
price: new BN(price * 10 ** PRICE_DECIMALS),
})
.accountsStrict({
owner,
receivingAccount,
transferAuthority: AdrenaClient.transferAuthority,
pool: AdrenaClient.mainPool,
position: position,
custody: custody.pubkey,
custodyTokenAccount,
custodyOracle: custody.oracle,
custodyTradeOracle: custody.tradeOracle,
tokenProgram: TOKEN_PROGRAM_ID,
lmStaking: AdrenaClient.lmStaking,
lpStaking: AdrenaClient.lpStaking,
cortex: AdrenaClient.cortex,
stakingRewardTokenCustody: stakingRewardTokenCustodyAccount.pubkey,
stakingRewardTokenCustodyOracle: stakingRewardTokenCustodyAccount.oracle,
stakingRewardTokenCustodyTokenAccount,
lmStakingRewardTokenVault: AdrenaClient.lmStakingRewardTokenVault,
lpStakingRewardTokenVault: AdrenaClient.lpStakingRewardTokenVault,
lpTokenMint: AdrenaClient.lpTokenMint,
protocolFeeRecipient: client.cortex.protocolFeeRecipient,
adrenaProgram: AdrenaClient.programId,
userProfile: userProfile ? userProfilePda : null,
caller: owner,
})
.instruction();
return sendTx(agent, [...preInstructions, instruction]);
}
/**
* Open long trade on Adrena
*
* Note: provide the same token as collateralMint and as tradeMint to avoid swap
* @returns Transaction signature
*/
export async function openPerpTradeLong({
agent,
price,
collateralAmount,
collateralMint = TOKENS.jitoSOL,
leverage = DEFAULT_OPTIONS.LEVERAGE_BPS,
tradeMint = TOKENS.jitoSOL,
slippage = 0.3,
}: {
agent: SolanaAgentKit;
price: number;
collateralAmount: number;
collateralMint?: PublicKey;
leverage?: number;
tradeMint?: PublicKey;
slippage?: number;
}): Promise<string> {
const client = await AdrenaClient.load(agent);
const owner = agent.wallet.publicKey;
const collateralAccount = AdrenaClient.findATAAddressSync(owner, tradeMint);
const fundingAccount = AdrenaClient.findATAAddressSync(owner, collateralMint);
const receivingCustody = AdrenaClient.findCustodyAddress(collateralMint);
const receivingCustodyOracle = client.getCustodyByMint(collateralMint).oracle;
const receivingCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(collateralMint);
// Principal custody is the custody of the targeted token
// i.e open a 1 ETH long position, principal custody is ETH
const principalCustody = AdrenaClient.findCustodyAddress(tradeMint);
const principalCustodyAccount = client.getCustodyByMint(tradeMint);
const principalCustodyOracle = principalCustodyAccount.oracle;
const principalCustodyTradeOracle = principalCustodyAccount.tradeOracle;
const principalCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(tradeMint);
const stakingRewardTokenCustodyAccount = client.getCustodyByMint(
AdrenaClient.stakingRewardTokenMint,
);
const stakingRewardTokenCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(
AdrenaClient.stakingRewardTokenMint,
);
const position = AdrenaClient.findPositionAddress(
owner,
principalCustody,
"long",
);
const userProfilePda = AdrenaClient.getUserProfilePda(owner);
const userProfile =
await client.program.account.userProfile.fetchNullable(userProfilePda);
const priceWithSlippage = applySlippage(
new BN(price * 10 ** PRICE_DECIMALS),
slippage,
);
const scaledCollateralAmount = new BN(
collateralAmount *
Math.pow(10, client.getCustodyByMint(collateralMint).decimals),
);
const preInstructions: TransactionInstruction[] = [];
if (
!(await AdrenaClient.isAccountInitialized(
agent.connection,
collateralAccount,
))
) {
preInstructions.push(
AdrenaClient.createATAInstruction({
ataAddress: collateralAccount,
mint: tradeMint,
owner,
}),
);
}
const instruction = await client.program.methods
.openOrIncreasePositionWithSwapLong({
price: priceWithSlippage,
collateral: scaledCollateralAmount,
leverage,
referrer: null,
})
.accountsStrict({
owner,
payer: owner,
fundingAccount,
collateralAccount,
receivingCustody,
receivingCustodyOracle,
receivingCustodyTokenAccount,
principalCustody,
principalCustodyOracle,
principalCustodyTradeOracle,
principalCustodyTokenAccount,
transferAuthority: AdrenaClient.transferAuthority,
cortex: AdrenaClient.cortex,
lmStaking: AdrenaClient.lmStaking,
lpStaking: AdrenaClient.lpStaking,
pool: AdrenaClient.mainPool,
position,
stakingRewardTokenCustody: stakingRewardTokenCustodyAccount.pubkey,
stakingRewardTokenCustodyOracle: stakingRewardTokenCustodyAccount.oracle,
stakingRewardTokenCustodyTokenAccount,
lmStakingRewardTokenVault: AdrenaClient.lmStakingRewardTokenVault,
lpStakingRewardTokenVault: AdrenaClient.lpStakingRewardTokenVault,
lpTokenMint: AdrenaClient.lpTokenMint,
userProfile: userProfile ? userProfilePda : null,
protocolFeeRecipient: client.cortex.protocolFeeRecipient,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
adrenaProgram: ADRENA_PROGRAM_ID,
})
.instruction();
return sendTx(agent, [...preInstructions, instruction]);
}
/**
* Open short trade on Adrena
*
* Note: provide USDC as collateralMint to avoid swap
* @returns Transaction signature
*/
export async function openPerpTradeShort({
agent,
price,
collateralAmount,
collateralMint = TOKENS.USDC,
leverage = DEFAULT_OPTIONS.LEVERAGE_BPS,
tradeMint = TOKENS.jitoSOL,
slippage = 0.3,
}: {
agent: SolanaAgentKit;
price: number;
collateralAmount: number;
collateralMint?: PublicKey;
leverage?: number;
tradeMint?: PublicKey;
slippage?: number;
}): Promise<string> {
const client = await AdrenaClient.load(agent);
const owner = agent.wallet.publicKey;
const collateralAccount = AdrenaClient.findATAAddressSync(owner, tradeMint);
const fundingAccount = AdrenaClient.findATAAddressSync(owner, collateralMint);
const receivingCustody = AdrenaClient.findCustodyAddress(collateralMint);
const receivingCustodyOracle = client.getCustodyByMint(collateralMint).oracle;
const receivingCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(collateralMint);
// Principal custody is the custody of the targeted token
// i.e open a 1 BTC short position, principal custody is BTC
const principalCustody = AdrenaClient.findCustodyAddress(tradeMint);
const principalCustodyAccount = client.getCustodyByMint(tradeMint);
const principalCustodyTradeOracle = principalCustodyAccount.tradeOracle;
const principalCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(tradeMint);
const usdcAta = AdrenaClient.findATAAddressSync(owner, TOKENS.USDC);
const preInstructions: TransactionInstruction[] = [];
if (!(await AdrenaClient.isAccountInitialized(agent.connection, usdcAta))) {
preInstructions.push(
AdrenaClient.createATAInstruction({
ataAddress: usdcAta,
mint: TOKENS.USDC,
owner,
}),
);
}
// Custody used to provide collateral when opening the position
// Should be a stable token, by default, use USDC
const instructionCollateralMint = TOKENS.USDC;
const collateralCustody = AdrenaClient.findCustodyAddress(
instructionCollateralMint,
);
const collateralCustodyOracle = client.getCustodyByMint(
instructionCollateralMint,
).oracle;
const collateralCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(instructionCollateralMint);
const stakingRewardTokenCustodyAccount = client.getCustodyByMint(
AdrenaClient.stakingRewardTokenMint,
);
const stakingRewardTokenCustodyTokenAccount =
AdrenaClient.findCustodyTokenAccountAddress(
AdrenaClient.stakingRewardTokenMint,
);
const position = AdrenaClient.findPositionAddress(
owner,
principalCustody,
"long",
);
const userProfilePda = AdrenaClient.getUserProfilePda(owner);
const userProfile =
await client.program.account.userProfile.fetchNullable(userProfilePda);
const priceWithSlippage = applySlippage(
new BN(price * 10 ** PRICE_DECIMALS),
slippage,
);
const scaledCollateralAmount = new BN(
collateralAmount *
Math.pow(10, client.getCustodyByMint(collateralMint).decimals),
);
const instruction = await client.program.methods
.openOrIncreasePositionWithSwapShort({
price: priceWithSlippage,
collateral: scaledCollateralAmount,
leverage,
referrer: null,
})
.accountsStrict({
owner,
payer: owner,
fundingAccount,
collateralAccount,
receivingCustody,
receivingCustodyOracle,
receivingCustodyTokenAccount,
principalCustody,
principalCustodyTradeOracle,
principalCustodyTokenAccount,
collateralCustody,
collateralCustodyOracle,
collateralCustodyTokenAccount,
transferAuthority: AdrenaClient.transferAuthority,
cortex: AdrenaClient.cortex,
lmStaking: AdrenaClient.lmStaking,
lpStaking: AdrenaClient.lpStaking,
pool: AdrenaClient.mainPool,
position,
stakingRewardTokenCustody: stakingRewardTokenCustodyAccount.pubkey,
stakingRewardTokenCustodyOracle: stakingRewardTokenCustodyAccount.oracle,
stakingRewardTokenCustodyTokenAccount,
lmStakingRewardTokenVault: AdrenaClient.lmStakingRewardTokenVault,
lpStakingRewardTokenVault: AdrenaClient.lpStakingRewardTokenVault,
lpTokenMint: AdrenaClient.lpTokenMint,
userProfile: userProfile ? userProfilePda : null,
protocolFeeRecipient: client.cortex.protocolFeeRecipient,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
adrenaProgram: ADRENA_PROGRAM_ID,
})
.instruction();
return sendTx(agent, [...preInstructions, instruction]);
}

View File

@@ -13,6 +13,7 @@ export * from "./limit_order";
export * from "./batch_order";
export * from "./cancel_all_orders";
export * from "./withdraw_all";
export * from "./adrena_perp_trading";
export * from "./register_domain";
export * from "./resolve_sol_domain";
export * from "./get_primary_domain";

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,
);
}
}