mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-27 23:26:50 +00:00
Merge branch 'main' into feat/squads_multisig
This commit is contained in:
506
src/tools/adrena_perp_trading.ts
Normal file
506
src/tools/adrena_perp_trading.ts
Normal 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]);
|
||||
}
|
||||
152
src/tools/batch_order.ts
Normal file
152
src/tools/batch_order.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
PublicKey,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import {
|
||||
ManifestClient,
|
||||
WrapperPlaceOrderParamsExternal,
|
||||
OrderType,
|
||||
} from "@cks-systems/manifest-sdk";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import { BatchOrderPattern, OrderParams } from "../types";
|
||||
|
||||
/**
|
||||
* Generates an array of orders based on the specified pattern
|
||||
*/
|
||||
export function generateOrdersfromPattern(
|
||||
pattern: BatchOrderPattern,
|
||||
): OrderParams[] {
|
||||
const orders: OrderParams[] = [];
|
||||
|
||||
// Random number of orders if not specified, max of 8
|
||||
const numOrders = pattern.numberOfOrders || Math.ceil(Math.random() * 8);
|
||||
|
||||
// Calculate price points
|
||||
const prices: number[] = [];
|
||||
if (pattern.priceRange) {
|
||||
const { min, max } = pattern.priceRange;
|
||||
if (min && max) {
|
||||
// Generate evenly spaced prices
|
||||
for (let i = 0; i < numOrders; i++) {
|
||||
if (pattern.spacing?.type === "percentage") {
|
||||
const factor = 1 + pattern.spacing.value / 100;
|
||||
prices.push(min * Math.pow(factor, i));
|
||||
} else {
|
||||
const step = (max - min) / (numOrders - 1);
|
||||
prices.push(min + step * i);
|
||||
}
|
||||
}
|
||||
} else if (min) {
|
||||
// Generate prices starting from min with specified spacing
|
||||
for (let i = 0; i < numOrders; i++) {
|
||||
if (pattern.spacing?.type === "percentage") {
|
||||
const factor = 1 + pattern.spacing.value / 100;
|
||||
prices.push(min * Math.pow(factor, i));
|
||||
} else {
|
||||
prices.push(min + (pattern.spacing?.value || 0.01) * i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate quantities
|
||||
let quantities: number[] = [];
|
||||
if (pattern.totalQuantity) {
|
||||
const individualQty = pattern.totalQuantity / numOrders;
|
||||
quantities = Array(numOrders).fill(individualQty);
|
||||
} else if (pattern.individualQuantity) {
|
||||
quantities = Array(numOrders).fill(pattern.individualQuantity);
|
||||
}
|
||||
|
||||
// Generate orders
|
||||
for (let i = 0; i < numOrders; i++) {
|
||||
orders.push({
|
||||
side: pattern.side,
|
||||
price: prices[i],
|
||||
quantity: quantities[i],
|
||||
});
|
||||
}
|
||||
|
||||
return orders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that sell orders are not priced below buy orders
|
||||
* @param orders Array of order parameters to validate
|
||||
* @throws Error if orders are crossed
|
||||
*/
|
||||
function validateNoCrossedOrders(orders: OrderParams[]): void {
|
||||
// Find lowest sell and highest buy prices
|
||||
let lowestSell = Number.MAX_SAFE_INTEGER;
|
||||
let highestBuy = 0;
|
||||
|
||||
orders.forEach((order) => {
|
||||
if (order.side === "Sell" && order.price < lowestSell) {
|
||||
lowestSell = order.price;
|
||||
}
|
||||
if (order.side === "Buy" && order.price > highestBuy) {
|
||||
highestBuy = order.price;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if orders cross
|
||||
if (lowestSell <= highestBuy) {
|
||||
throw new Error(
|
||||
`Invalid order prices: Sell order at ${lowestSell} is lower than or equal to Buy order at ${highestBuy}. Orders cannot cross.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place batch orders using Manifest
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param marketId Public key for the manifest market
|
||||
* @param quantity Amount to trade in tokens
|
||||
* @param side Buy or Sell
|
||||
* @param price Price in tokens ie. SOL/USDC
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
export async function batchOrder(
|
||||
agent: SolanaAgentKit,
|
||||
marketId: PublicKey,
|
||||
orders: OrderParams[],
|
||||
): Promise<string> {
|
||||
try {
|
||||
validateNoCrossedOrders(orders);
|
||||
|
||||
const mfxClient = await ManifestClient.getClientForMarket(
|
||||
agent.connection,
|
||||
marketId,
|
||||
agent.wallet,
|
||||
);
|
||||
|
||||
const placeParams: WrapperPlaceOrderParamsExternal[] = orders.map(
|
||||
(order) => ({
|
||||
numBaseTokens: order.quantity,
|
||||
tokenPrice: order.price,
|
||||
isBid: order.side === "Buy",
|
||||
lastValidSlot: 0,
|
||||
orderType: OrderType.Limit,
|
||||
clientOrderId: Number(Math.random() * 10000),
|
||||
}),
|
||||
);
|
||||
|
||||
const batchOrderIx: TransactionInstruction = await mfxClient.batchUpdateIx(
|
||||
placeParams,
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
const signature = await sendAndConfirmTransaction(
|
||||
agent.connection,
|
||||
new Transaction().add(batchOrderIx),
|
||||
[agent.wallet],
|
||||
);
|
||||
|
||||
return signature;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Batch Order failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
37
src/tools/cancel_all_orders.ts
Normal file
37
src/tools/cancel_all_orders.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
PublicKey,
|
||||
sendAndConfirmTransaction,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import { ManifestClient } from "@cks-systems/manifest-sdk";
|
||||
|
||||
/**
|
||||
* Cancels all orders from Manifest
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param marketId Public key for the manifest market
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
export async function cancelAllOrders(
|
||||
agent: SolanaAgentKit,
|
||||
marketId: PublicKey,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const mfxClient = await ManifestClient.getClientForMarket(
|
||||
agent.connection,
|
||||
marketId,
|
||||
agent.wallet,
|
||||
);
|
||||
|
||||
const cancelAllOrdersIx = await mfxClient.cancelAllIx();
|
||||
const signature = await sendAndConfirmTransaction(
|
||||
agent.connection,
|
||||
new Transaction().add(cancelAllOrdersIx),
|
||||
[agent.wallet],
|
||||
);
|
||||
|
||||
return signature;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Cancel all orders failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,12 @@ export async function create_image(
|
||||
n: number = 1,
|
||||
) {
|
||||
try {
|
||||
if (!agent.openai_api_key) {
|
||||
if (!agent.config.OPENAI_API_KEY) {
|
||||
throw new Error("OpenAI API key not found in agent configuration");
|
||||
}
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: agent.openai_api_key,
|
||||
apiKey: agent.config.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
const response = await openai.images.generate({
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
import { Keypair, PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import { BN, Wallet } from "@coral-xyz/anchor";
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
PDAUtil,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
ORCA_WHIRLPOOLS_CONFIG,
|
||||
WhirlpoolContext,
|
||||
TickUtil,
|
||||
PriceMath,
|
||||
PoolUtil,
|
||||
TokenExtensionContextForPool,
|
||||
NO_TOKEN_EXTENSION_CONTEXT,
|
||||
TokenExtensionUtil,
|
||||
WhirlpoolIx,
|
||||
IncreaseLiquidityQuoteParam,
|
||||
increaseLiquidityQuoteByInputTokenWithParams,
|
||||
} from "@orca-so/whirlpools-sdk";
|
||||
import {
|
||||
Percentage,
|
||||
resolveOrCreateATAs,
|
||||
TransactionBuilder,
|
||||
} from "@orca-so/common-sdk";
|
||||
import {
|
||||
increaseLiquidityIx,
|
||||
increaseLiquidityV2Ix,
|
||||
initTickArrayIx,
|
||||
openPositionWithTokenExtensionsIx,
|
||||
} from "@orca-so/whirlpools-sdk/dist/instructions";
|
||||
import {
|
||||
getAssociatedTokenAddressSync,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { sendTx } from "../utils/send_tx";
|
||||
|
||||
/**
|
||||
* Maps fee tier percentages to their corresponding tick spacing values in the Orca Whirlpool protocol.
|
||||
*
|
||||
* @remarks
|
||||
* Fee tiers determine the percentage of fees collected on swaps, while tick spacing affects
|
||||
* the granularity of price ranges for liquidity positions.
|
||||
*
|
||||
* For more details, refer to:
|
||||
* - [Whirlpool Fees](https://orca-so.github.io/whirlpools/Architecture%20Overview/Whirlpool%20Fees)
|
||||
* - [Whirlpool Parameters](https://orca-so.github.io/whirlpools/Architecture%20Overview/Whirlpool%20Parameters)
|
||||
*
|
||||
* @example
|
||||
* const tickSpacing = FEE_TIERS[0.30]; // Returns 64
|
||||
*/
|
||||
export const FEE_TIERS = {
|
||||
0.01: 1,
|
||||
0.02: 2,
|
||||
0.04: 4,
|
||||
0.05: 8,
|
||||
0.16: 16,
|
||||
0.3: 64,
|
||||
0.65: 96,
|
||||
1.0: 128,
|
||||
2.0: 256,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* # Creates a single-sided Whirlpool.
|
||||
*
|
||||
* This function initializes a new Whirlpool (liquidity pool) on Orca and seeds it with liquidity from a single token.
|
||||
*
|
||||
* ## Example Usage:
|
||||
* You created a new token called SHARK, and you want to set the initial price to 0.001 USDC.
|
||||
* You set `depositTokenMint` to SHARK's mint address and `otherTokenMint` to USDC's mint address.
|
||||
* You can minimize price impact for buyers in a few ways:
|
||||
* 1. Increase the amount of tokens you deposit
|
||||
* 2. Set the initial price very low
|
||||
* 3. Set the maximum price closer to the initial price
|
||||
*
|
||||
* ### Note for experts:
|
||||
* The Wrhirlpool program initializes the Whirlpool with the in a specific order. This might not be
|
||||
* the order you expect, so the function checks the order and adjusts the inverts the prices. This means that
|
||||
* on-chain the Whirlpool might be configured as USDC/SHARK instead of SHARK/USDC, and the on-chain price will
|
||||
* be 1/`initialPrice`. This will not affect the price of the token as you intended it to be.
|
||||
*
|
||||
* @param agent - The `SolanaAgentKit` instance representing the wallet and connection details.
|
||||
* @param depositTokenAmount - The amount of the deposit token (including the decimals) to contribute to the pool.
|
||||
* @param depositTokenMint - The mint address of the token being deposited into the pool, eg. SHARK.
|
||||
* @param otherTokenMint - The mint address of the other token in the pool, eg. USDC.
|
||||
* @param initialPrice - The initial price of the deposit token in terms of the other token.
|
||||
* @param maxPrice - The maximum price at which liquidity is added.
|
||||
* @param feeTier - The fee tier percentage for the pool, determining tick spacing and fee collection rates.
|
||||
*
|
||||
* @returns A promise that resolves to a transaction ID (`string`) of the transaction creating the pool.
|
||||
*
|
||||
* @throws Will throw an error if:
|
||||
* - Mint accounts for the tokens cannot be fetched.
|
||||
* - Prices are out of bounds.
|
||||
*
|
||||
* @remarks
|
||||
* This function is designed for single-sided deposits where users only contribute one type of token,
|
||||
* and the function manages mint order and necessary calculations.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { SolanaAgentKit } from "your-sdk";
|
||||
* import { PublicKey } from "@solana/web3.js";
|
||||
* import { BN } from "@coral-xyz/anchor";
|
||||
* import Decimal from "decimal.js";
|
||||
*
|
||||
* const agent = new SolanaAgentKit(wallet, connection);
|
||||
* const depositAmount = new BN(1_000_000_000_000); // 1 million SHARK if SHARK has 6 decimals
|
||||
* const depositTokenMint = new PublicKey("DEPOSTI_TOKEN_ADDRESS");
|
||||
* const otherTokenMint = new PublicKey("OTHER_TOKEN_ADDRESS");
|
||||
* const initialPrice = new Decimal(0.001);
|
||||
* const maxPrice = new Decimal(5.0);
|
||||
* const feeTier = 0.30;
|
||||
*
|
||||
* const txId = await createOrcaSingleSidedWhirlpool(
|
||||
* agent,
|
||||
* depositAmount,
|
||||
* depositTokenMint,
|
||||
* otherTokenMint,
|
||||
* initialPrice,
|
||||
* maxPrice,
|
||||
* feeTier,
|
||||
* );
|
||||
* console.log(`Single sided whirlpool created in transaction: ${txId}`);
|
||||
* ```
|
||||
*/
|
||||
export async function createOrcaSingleSidedWhirlpool(
|
||||
agent: SolanaAgentKit,
|
||||
depositTokenAmount: BN,
|
||||
depositTokenMint: PublicKey,
|
||||
otherTokenMint: PublicKey,
|
||||
initialPrice: Decimal,
|
||||
maxPrice: Decimal,
|
||||
feeTier: keyof typeof FEE_TIERS,
|
||||
): Promise<string> {
|
||||
const wallet = new Wallet(agent.wallet);
|
||||
const ctx = WhirlpoolContext.from(
|
||||
agent.connection,
|
||||
wallet,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
);
|
||||
const fetcher = ctx.fetcher;
|
||||
|
||||
const correctTokenOrder = PoolUtil.orderMints(
|
||||
otherTokenMint,
|
||||
depositTokenMint,
|
||||
).map((addr) => addr.toString());
|
||||
const isCorrectMintOrder =
|
||||
correctTokenOrder[0] === depositTokenMint.toString();
|
||||
let mintA, mintB;
|
||||
if (isCorrectMintOrder) {
|
||||
[mintA, mintB] = [depositTokenMint, otherTokenMint];
|
||||
} else {
|
||||
[mintA, mintB] = [otherTokenMint, depositTokenMint];
|
||||
initialPrice = new Decimal(1 / initialPrice.toNumber());
|
||||
maxPrice = new Decimal(1 / maxPrice.toNumber());
|
||||
}
|
||||
const mintAAccount = await fetcher.getMintInfo(mintA);
|
||||
const mintBAccount = await fetcher.getMintInfo(mintB);
|
||||
if (mintAAccount === null || mintBAccount === null) {
|
||||
throw Error("Mint account not found");
|
||||
}
|
||||
const tickSpacing = FEE_TIERS[feeTier];
|
||||
const tickIndex = PriceMath.priceToTickIndex(
|
||||
initialPrice,
|
||||
mintAAccount.decimals,
|
||||
mintBAccount.decimals,
|
||||
);
|
||||
const initialTick = TickUtil.getInitializableTickIndex(
|
||||
tickIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
|
||||
const tokenExtensionCtx: TokenExtensionContextForPool = {
|
||||
...NO_TOKEN_EXTENSION_CONTEXT,
|
||||
tokenMintWithProgramA: mintAAccount,
|
||||
tokenMintWithProgramB: mintBAccount,
|
||||
};
|
||||
const feeTierKey = PDAUtil.getFeeTier(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
ORCA_WHIRLPOOLS_CONFIG,
|
||||
tickSpacing,
|
||||
).publicKey;
|
||||
const initSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(initialTick);
|
||||
const tokenVaultAKeypair = Keypair.generate();
|
||||
const tokenVaultBKeypair = Keypair.generate();
|
||||
const whirlpoolPda = PDAUtil.getWhirlpool(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
ORCA_WHIRLPOOLS_CONFIG,
|
||||
mintA,
|
||||
mintB,
|
||||
FEE_TIERS[feeTier],
|
||||
);
|
||||
const tokenBadgeA = PDAUtil.getTokenBadge(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
ORCA_WHIRLPOOLS_CONFIG,
|
||||
mintA,
|
||||
).publicKey;
|
||||
const tokenBadgeB = PDAUtil.getTokenBadge(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
ORCA_WHIRLPOOLS_CONFIG,
|
||||
mintB,
|
||||
).publicKey;
|
||||
const baseParamsPool = {
|
||||
initSqrtPrice,
|
||||
whirlpoolsConfig: ORCA_WHIRLPOOLS_CONFIG,
|
||||
whirlpoolPda,
|
||||
tokenMintA: mintA,
|
||||
tokenMintB: mintB,
|
||||
tokenVaultAKeypair,
|
||||
tokenVaultBKeypair,
|
||||
feeTierKey,
|
||||
tickSpacing: tickSpacing,
|
||||
funder: wallet.publicKey,
|
||||
};
|
||||
const initPoolIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx)
|
||||
? WhirlpoolIx.initializePoolIx(ctx.program, baseParamsPool)
|
||||
: WhirlpoolIx.initializePoolV2Ix(ctx.program, {
|
||||
...baseParamsPool,
|
||||
tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram,
|
||||
tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram,
|
||||
tokenBadgeA,
|
||||
tokenBadgeB,
|
||||
});
|
||||
const initialTickArrayStartTick = TickUtil.getStartTickIndex(
|
||||
initialTick,
|
||||
tickSpacing,
|
||||
);
|
||||
const initialTickArrayPda = PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolPda.publicKey,
|
||||
initialTickArrayStartTick,
|
||||
);
|
||||
|
||||
const txBuilder = new TransactionBuilder(
|
||||
ctx.provider.connection,
|
||||
ctx.provider.wallet,
|
||||
ctx.txBuilderOpts,
|
||||
);
|
||||
txBuilder.addInstruction(initPoolIx);
|
||||
txBuilder.addInstruction(
|
||||
initTickArrayIx(ctx.program, {
|
||||
startTick: initialTickArrayStartTick,
|
||||
tickArrayPda: initialTickArrayPda,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
funder: wallet.publicKey,
|
||||
}),
|
||||
);
|
||||
|
||||
let tickLowerIndex, tickUpperIndex;
|
||||
if (isCorrectMintOrder) {
|
||||
tickLowerIndex = initialTick;
|
||||
tickUpperIndex = PriceMath.priceToTickIndex(
|
||||
maxPrice,
|
||||
mintAAccount.decimals,
|
||||
mintBAccount.decimals,
|
||||
);
|
||||
} else {
|
||||
tickLowerIndex = PriceMath.priceToTickIndex(
|
||||
maxPrice,
|
||||
mintAAccount.decimals,
|
||||
mintBAccount.decimals,
|
||||
);
|
||||
tickUpperIndex = initialTick;
|
||||
}
|
||||
const tickLowerInitializableIndex = TickUtil.getInitializableTickIndex(
|
||||
tickLowerIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
const tickUpperInitializableIndex = TickUtil.getInitializableTickIndex(
|
||||
tickUpperIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
if (
|
||||
!TickUtil.checkTickInBounds(tickLowerInitializableIndex) ||
|
||||
!TickUtil.checkTickInBounds(tickUpperInitializableIndex)
|
||||
) {
|
||||
throw Error("Prices out of bounds");
|
||||
}
|
||||
const increasLiquidityQuoteParam: IncreaseLiquidityQuoteParam = {
|
||||
inputTokenAmount: new BN(depositTokenAmount),
|
||||
inputTokenMint: depositTokenMint,
|
||||
tokenMintA: mintA,
|
||||
tokenMintB: mintB,
|
||||
tickCurrentIndex: initialTick,
|
||||
sqrtPrice: initSqrtPrice,
|
||||
tickLowerIndex: tickLowerInitializableIndex,
|
||||
tickUpperIndex: tickUpperInitializableIndex,
|
||||
tokenExtensionCtx: tokenExtensionCtx,
|
||||
slippageTolerance: Percentage.fromFraction(0, 100),
|
||||
};
|
||||
const liquidityInput = increaseLiquidityQuoteByInputTokenWithParams(
|
||||
increasLiquidityQuoteParam,
|
||||
);
|
||||
const { liquidityAmount: liquidity, tokenMaxA, tokenMaxB } = liquidityInput;
|
||||
|
||||
const positionMintKeypair = Keypair.generate();
|
||||
const positionMintPubkey = positionMintKeypair.publicKey;
|
||||
const positionPda = PDAUtil.getPosition(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
positionMintPubkey,
|
||||
);
|
||||
const positionTokenAccountAddress = getAssociatedTokenAddressSync(
|
||||
positionMintPubkey,
|
||||
wallet.publicKey,
|
||||
ctx.accountResolverOpts.allowPDAOwnerAddress,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
);
|
||||
const params = {
|
||||
funder: wallet.publicKey,
|
||||
owner: wallet.publicKey,
|
||||
positionPda,
|
||||
positionTokenAccount: positionTokenAccountAddress,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
tickLowerIndex: tickLowerInitializableIndex,
|
||||
tickUpperIndex: tickUpperInitializableIndex,
|
||||
};
|
||||
const positionIx = openPositionWithTokenExtensionsIx(ctx.program, {
|
||||
...params,
|
||||
positionMint: positionMintPubkey,
|
||||
withTokenMetadataExtension: true,
|
||||
});
|
||||
|
||||
txBuilder.addInstruction(positionIx);
|
||||
txBuilder.addSigner(positionMintKeypair);
|
||||
|
||||
const [ataA, ataB] = await resolveOrCreateATAs(
|
||||
ctx.connection,
|
||||
wallet.publicKey,
|
||||
[
|
||||
{ tokenMint: mintA, wrappedSolAmountIn: tokenMaxA },
|
||||
{ tokenMint: mintB, wrappedSolAmountIn: tokenMaxB },
|
||||
],
|
||||
() => ctx.fetcher.getAccountRentExempt(),
|
||||
wallet.publicKey,
|
||||
undefined,
|
||||
ctx.accountResolverOpts.allowPDAOwnerAddress,
|
||||
ctx.accountResolverOpts.createWrappedSolAccountMethod,
|
||||
);
|
||||
const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA;
|
||||
const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = ataB;
|
||||
|
||||
txBuilder.addInstruction(tokenOwnerAccountAIx);
|
||||
txBuilder.addInstruction(tokenOwnerAccountBIx);
|
||||
|
||||
const tickArrayLowerStartIndex = TickUtil.getStartTickIndex(
|
||||
tickLowerInitializableIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
const tickArrayUpperStartIndex = TickUtil.getStartTickIndex(
|
||||
tickUpperInitializableIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
const tickArrayLowerPda = PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolPda.publicKey,
|
||||
tickArrayLowerStartIndex,
|
||||
);
|
||||
const tickArrayUpperPda = PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolPda.publicKey,
|
||||
tickArrayUpperStartIndex,
|
||||
);
|
||||
if (tickArrayUpperStartIndex !== tickArrayLowerStartIndex) {
|
||||
if (isCorrectMintOrder) {
|
||||
txBuilder.addInstruction(
|
||||
initTickArrayIx(ctx.program, {
|
||||
startTick: tickArrayUpperStartIndex,
|
||||
tickArrayPda: tickArrayUpperPda,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
funder: wallet.publicKey,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
txBuilder.addInstruction(
|
||||
initTickArrayIx(ctx.program, {
|
||||
startTick: tickArrayLowerStartIndex,
|
||||
tickArrayPda: tickArrayLowerPda,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
funder: wallet.publicKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const baseParamsLiquidity = {
|
||||
liquidityAmount: liquidity,
|
||||
tokenMaxA,
|
||||
tokenMaxB,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
positionAuthority: wallet.publicKey,
|
||||
position: positionPda.publicKey,
|
||||
positionTokenAccount: positionTokenAccountAddress,
|
||||
tokenOwnerAccountA,
|
||||
tokenOwnerAccountB,
|
||||
tokenVaultA: tokenVaultAKeypair.publicKey,
|
||||
tokenVaultB: tokenVaultBKeypair.publicKey,
|
||||
tickArrayLower: tickArrayLowerPda.publicKey,
|
||||
tickArrayUpper: tickArrayUpperPda.publicKey,
|
||||
};
|
||||
|
||||
const liquidityIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx)
|
||||
? increaseLiquidityIx(ctx.program, baseParamsLiquidity)
|
||||
: increaseLiquidityV2Ix(ctx.program, {
|
||||
...baseParamsLiquidity,
|
||||
tokenMintA: mintA,
|
||||
tokenMintB: mintB,
|
||||
tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram,
|
||||
tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram,
|
||||
});
|
||||
txBuilder.addInstruction(liquidityIx);
|
||||
|
||||
const txPayload = await txBuilder.build({
|
||||
maxSupportedTransactionVersion: "legacy",
|
||||
});
|
||||
|
||||
if (txPayload.transaction instanceof Transaction) {
|
||||
try {
|
||||
const txId = await sendTx(agent, txPayload.transaction, [
|
||||
positionMintKeypair,
|
||||
tokenVaultAKeypair,
|
||||
tokenVaultBKeypair,
|
||||
]);
|
||||
return txId;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create pool: ${JSON.stringify(error)}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to create pool: Transaction not created");
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export async function deploy_token(
|
||||
mint: mint.publicKey,
|
||||
tokenStandard: TokenStandard.Fungible,
|
||||
tokenOwner: fromWeb3JsPublicKey(agent.wallet_address),
|
||||
amount: initialSupply,
|
||||
amount: initialSupply * Math.pow(10, decimals),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ import { getAllTld } from "@onsol/tldparser";
|
||||
*/
|
||||
export async function getAllDomainsTLDs(
|
||||
agent: SolanaAgentKit,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): Promise<String[]> {
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const tlds = await getAllTld(agent.connection);
|
||||
return tlds.map((tld) => tld.tld);
|
||||
return tlds.map((tld) => String(tld.tld));
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to fetch TLDs: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export async function getMainAllDomainsDomain(
|
||||
mainDomain = await _getFavoriteDomain(agent.connection, owner);
|
||||
return mainDomain.stale ? null : mainDomain.reverse;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ export async function getPrimaryDomain(
|
||||
);
|
||||
}
|
||||
return reverse;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
throw new Error(
|
||||
`Failed to get primary domain for account: ${account.toBase58()}`,
|
||||
);
|
||||
|
||||
10
src/tools/get_wallet_address.ts
Normal file
10
src/tools/get_wallet_address.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
|
||||
/**
|
||||
* Get the agents wallet address
|
||||
* @param agent - SolanaAgentKit instance
|
||||
* @returns string
|
||||
*/
|
||||
export function get_wallet_address(agent: SolanaAgentKit) {
|
||||
return agent.wallet_address.toBase58();
|
||||
}
|
||||
@@ -1,43 +1,49 @@
|
||||
export * from "./request_faucet_funds";
|
||||
export * from "./deploy_token";
|
||||
export * from "./adrena_perp_trading";
|
||||
export * from "./batch_order";
|
||||
export * from "./cancel_all_orders";
|
||||
export * from "./create_gibwork_task";
|
||||
export * from "./create_image";
|
||||
export * from "./create_tiplinks";
|
||||
export * from "./deploy_collection";
|
||||
export * from "./deploy_token";
|
||||
export * from "./fetch_price";
|
||||
export * from "./get_all_domains_tlds";
|
||||
export * from "./get_all_registered_all_domains";
|
||||
export * from "./get_balance";
|
||||
export * from "./get_balance_other";
|
||||
export * from "./mint_nft";
|
||||
export * from "./transfer";
|
||||
export * from "./trade";
|
||||
export * from "./register_domain";
|
||||
export * from "./resolve_sol_domain";
|
||||
export * from "./get_main_all_domains_domain";
|
||||
export * from "./get_owned_all_domains";
|
||||
export * from "./get_owned_domains_for_tld";
|
||||
export * from "./get_primary_domain";
|
||||
export * from "./get_token_data";
|
||||
export * from "./get_tps";
|
||||
export * from "./get_wallet_address";
|
||||
export * from "./launch_pumpfun_token";
|
||||
export * from "./lend";
|
||||
export * from "./get_tps";
|
||||
export * from "./get_token_data";
|
||||
export * from "./stake_with_jup";
|
||||
export * from "./fetch_price";
|
||||
export * from "./send_compressed_airdrop";
|
||||
export * from "./create_orca_single_sided_whirlpool";
|
||||
export * from "./get_all_domains_tlds";
|
||||
export * from "./get_all_registered_all_domains";
|
||||
export * from "./get_owned_domains_for_tld";
|
||||
export * from "./get_main_all_domains_domain";
|
||||
export * from "./get_owned_all_domains";
|
||||
export * from "./resolve_domain";
|
||||
|
||||
export * from "./get_all_domains_tlds";
|
||||
export * from "./get_all_registered_all_domains";
|
||||
export * from "./get_owned_domains_for_tld";
|
||||
export * from "./get_main_all_domains_domain";
|
||||
export * from "./get_owned_all_domains";
|
||||
export * from "./resolve_domain";
|
||||
|
||||
export * from "./limit_order";
|
||||
export * from "./manifest_create_market";
|
||||
export * from "./mint_nft";
|
||||
export * from "./openbook_create_market";
|
||||
export * from "./orca_close_position";
|
||||
export * from "./orca_create_clmm";
|
||||
export * from "./orca_create_single_sided_liquidity_pool";
|
||||
export * from "./orca_fetch_positions";
|
||||
export * from "./orca_open_centered_position_with_liquidity";
|
||||
export * from "./orca_open_single_sided_position";
|
||||
export * from "./pyth_fetch_price";
|
||||
export * from "./raydium_create_ammV4";
|
||||
export * from "./raydium_create_clmm";
|
||||
export * from "./raydium_create_cpmm";
|
||||
export * from "./openbook_create_market";
|
||||
export * from "./pyth_fetch_price";
|
||||
|
||||
export * from "./create_gibwork_task";
|
||||
|
||||
export * from "./register_domain";
|
||||
export * from "./request_faucet_funds";
|
||||
export * from "./resolve_domain";
|
||||
export * from "./resolve_sol_domain";
|
||||
export * from "./rock_paper_scissor";
|
||||
export * from "./create_tiplinks";
|
||||
export * from "./rugcheck";
|
||||
export * from "./send_compressed_airdrop";
|
||||
export * from "./stake_with_jup";
|
||||
export * from "./stake_with_solayer";
|
||||
export * from "./tensor_trade";
|
||||
export * from "./trade";
|
||||
export * from "./transfer";
|
||||
export * from "./withdraw_all";
|
||||
|
||||
61
src/tools/limit_order.ts
Normal file
61
src/tools/limit_order.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
PublicKey,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import {
|
||||
ManifestClient,
|
||||
WrapperPlaceOrderParamsExternal,
|
||||
OrderType,
|
||||
} from "@cks-systems/manifest-sdk";
|
||||
|
||||
/**
|
||||
* Place limit orders using Manifest
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param marketId Public key for the manifest market
|
||||
* @param quantity Amount to trade in tokens
|
||||
* @param side Buy or Sell
|
||||
* @param price Price in tokens ie. SOL/USDC
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
export async function limitOrder(
|
||||
agent: SolanaAgentKit,
|
||||
marketId: PublicKey,
|
||||
quantity: number,
|
||||
side: string,
|
||||
price: number,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const mfxClient = await ManifestClient.getClientForMarket(
|
||||
agent.connection,
|
||||
marketId,
|
||||
agent.wallet,
|
||||
);
|
||||
|
||||
const orderParams: WrapperPlaceOrderParamsExternal = {
|
||||
numBaseTokens: quantity,
|
||||
tokenPrice: price,
|
||||
isBid: side === "Buy",
|
||||
lastValidSlot: 0,
|
||||
orderType: OrderType.Limit,
|
||||
clientOrderId: Number(Math.random() * 1000),
|
||||
};
|
||||
|
||||
const depositPlaceOrderIx: TransactionInstruction[] =
|
||||
await mfxClient.placeOrderWithRequiredDepositIx(
|
||||
agent.wallet.publicKey,
|
||||
orderParams,
|
||||
);
|
||||
const signature = await sendAndConfirmTransaction(
|
||||
agent.connection,
|
||||
new Transaction().add(...depositPlaceOrderIx),
|
||||
[agent.wallet],
|
||||
);
|
||||
|
||||
return signature;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Limit Order failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
43
src/tools/manifest_create_market.ts
Normal file
43
src/tools/manifest_create_market.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ManifestClient } from "@cks-systems/manifest-sdk";
|
||||
import {
|
||||
Keypair,
|
||||
PublicKey,
|
||||
sendAndConfirmTransaction,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
|
||||
export async function manifestCreateMarket(
|
||||
agent: SolanaAgentKit,
|
||||
baseMint: PublicKey,
|
||||
quoteMint: PublicKey,
|
||||
): Promise<string[]> {
|
||||
const marketKeypair: Keypair = Keypair.generate();
|
||||
const FIXED_MANIFEST_HEADER_SIZE: number = 256;
|
||||
const createAccountIx: TransactionInstruction = SystemProgram.createAccount({
|
||||
fromPubkey: agent.wallet.publicKey,
|
||||
newAccountPubkey: marketKeypair.publicKey,
|
||||
space: FIXED_MANIFEST_HEADER_SIZE,
|
||||
lamports: await agent.connection.getMinimumBalanceForRentExemption(
|
||||
FIXED_MANIFEST_HEADER_SIZE,
|
||||
),
|
||||
programId: new PublicKey("MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms"),
|
||||
});
|
||||
const createMarketIx = ManifestClient["createMarketIx"](
|
||||
agent.wallet.publicKey,
|
||||
baseMint,
|
||||
quoteMint,
|
||||
marketKeypair.publicKey,
|
||||
);
|
||||
|
||||
const tx: Transaction = new Transaction();
|
||||
tx.add(createAccountIx);
|
||||
tx.add(createMarketIx);
|
||||
const signature = await sendAndConfirmTransaction(agent.connection, tx, [
|
||||
agent.wallet,
|
||||
marketKeypair,
|
||||
]);
|
||||
return [signature, marketKeypair.publicKey.toBase58()];
|
||||
}
|
||||
82
src/tools/orca_close_position.ts
Normal file
82
src/tools/orca_close_position.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Keypair,
|
||||
PublicKey,
|
||||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
import { Wallet } from "@coral-xyz/anchor";
|
||||
import {
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
WhirlpoolContext,
|
||||
buildWhirlpoolClient,
|
||||
PDAUtil,
|
||||
} from "@orca-so/whirlpools-sdk";
|
||||
import { sendTx } from "../utils/send_tx";
|
||||
import { Percentage } from "@orca-so/common-sdk";
|
||||
|
||||
/**
|
||||
* # Closes a Liquidity Position in an Orca Whirlpool
|
||||
*
|
||||
* This function closes an existing liquidity position in a specified Orca Whirlpool. The user provides
|
||||
* the position's mint address.
|
||||
*
|
||||
* ## Parameters
|
||||
* - `agent`: The `SolanaAgentKit` instance representing the wallet and connection details.
|
||||
* - `positionMintAddress`: The mint address of the liquidity position to close.
|
||||
*
|
||||
* ## Returns
|
||||
* A `Promise` that resolves to a `string` containing the transaction ID of the transaction
|
||||
*
|
||||
* ## Notes
|
||||
* - The function uses Orca’s SDK to interact with the specified Whirlpool and close the liquidity position.
|
||||
* - A maximum slippage of 1% is assumed for liquidity provision during the position closing.
|
||||
* - The function automatically fetches the associated Whirlpool address and position details using the provided mint address.
|
||||
*
|
||||
* ## Throws
|
||||
* An error will be thrown if:
|
||||
* - The specified position mint address is invalid or inaccessible.
|
||||
* - The transaction fails to send.
|
||||
* - Any required position or Whirlpool data cannot be fetched.
|
||||
*
|
||||
* @param agent - The `SolanaAgentKit` instance representing the wallet and connection.
|
||||
* @param positionMintAddress - The mint address of the liquidity position to close.
|
||||
* @returns A promise resolving to the transaction ID (`string`).
|
||||
*/
|
||||
export async function orcaClosePosition(
|
||||
agent: SolanaAgentKit,
|
||||
positionMintAddress: PublicKey,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const wallet = new Wallet(agent.wallet);
|
||||
const ctx = WhirlpoolContext.from(
|
||||
agent.connection,
|
||||
wallet,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
);
|
||||
const client = buildWhirlpoolClient(ctx);
|
||||
|
||||
const positionAddress = PDAUtil.getPosition(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
positionMintAddress,
|
||||
);
|
||||
const position = await client.getPosition(positionAddress.publicKey);
|
||||
const whirlpoolAddress = position.getData().whirlpool;
|
||||
const whirlpool = await client.getPool(whirlpoolAddress);
|
||||
const txBuilder = await whirlpool.closePosition(
|
||||
positionAddress.publicKey,
|
||||
Percentage.fromFraction(1, 100),
|
||||
);
|
||||
const txPayload = await txBuilder[0].build();
|
||||
const txPayloadDecompiled = TransactionMessage.decompile(
|
||||
(txPayload.transaction as VersionedTransaction).message,
|
||||
);
|
||||
const instructions = txPayloadDecompiled.instructions;
|
||||
const signers = txPayload.signers as Keypair[];
|
||||
|
||||
const txId = await sendTx(agent, instructions, signers);
|
||||
return txId;
|
||||
} catch (error) {
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
132
src/tools/orca_create_clmm.ts
Normal file
132
src/tools/orca_create_clmm.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
Keypair,
|
||||
PublicKey,
|
||||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
import { Wallet } from "@coral-xyz/anchor";
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
WhirlpoolContext,
|
||||
PriceMath,
|
||||
PoolUtil,
|
||||
buildWhirlpoolClient,
|
||||
} from "@orca-so/whirlpools-sdk";
|
||||
import { sendTx } from "../utils/send_tx";
|
||||
import { FEE_TIERS } from "./orca_create_single_sided_liquidity_pool";
|
||||
|
||||
/**
|
||||
* # Creates a CLMM Pool (Concentrated Liquidity Market Maker Pool).
|
||||
*
|
||||
* This function initializes a new Whirlpool (CLMM Pool) on Orca. It only sets up the pool and does not seed it with liquidity.
|
||||
*
|
||||
* ## Example Usage:
|
||||
* Suppose you want to create a CLMM pool with two tokens, SHARK and USDC, and set the initial price of SHARK to 0.001 USDC.
|
||||
* You would call this function with `mintA` as SHARK's mint address and `mintB` as USDC's mint address. The pool is created
|
||||
* with the specified fee tier and tick spacing associated with that fee tier.
|
||||
*
|
||||
* ### Note for Experts:
|
||||
* The Whirlpool program determines the token mint order, which might not match your expectation. This function
|
||||
* adjusts the input order as needed and inverts the initial price accordingly.
|
||||
*
|
||||
* @param agent - The `SolanaAgentKit` instance representing the wallet and connection details.
|
||||
* @param mintDeploy - The mint of the token you want to deploy (e.g., SHARK).
|
||||
* @param mintPair - The mint of the token you want to pair the deployed mint with (e.g., USDC).
|
||||
* @param initialPrice - The initial price of `mintDeploy` in terms of `mintPair`.
|
||||
* @param feeTier - The fee tier bps for the pool, determining tick spacing and fee collection rates.
|
||||
*
|
||||
* @returns A promise that resolves to a transaction ID (`string`) of the transaction creating the pool.
|
||||
*
|
||||
* @throws Will throw an error if:
|
||||
* - Mint accounts for the tokens cannot be fetched.
|
||||
* - The network is unsupported.
|
||||
*
|
||||
* @remarks
|
||||
* This function only initializes the CLMM pool and does not add liquidity. For adding liquidity, you can use
|
||||
* a separate function after the pool is successfully created.
|
||||
* ```
|
||||
*/
|
||||
export async function orcaCreateCLMM(
|
||||
agent: SolanaAgentKit,
|
||||
mintDeploy: PublicKey,
|
||||
mintPair: PublicKey,
|
||||
initialPrice: Decimal,
|
||||
feeTier: keyof typeof FEE_TIERS,
|
||||
): Promise<string> {
|
||||
try {
|
||||
let whirlpoolsConfigAddress: PublicKey;
|
||||
if (agent.connection.rpcEndpoint.includes("mainnet")) {
|
||||
whirlpoolsConfigAddress = new PublicKey(
|
||||
"2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ",
|
||||
);
|
||||
} else if (agent.connection.rpcEndpoint.includes("devnet")) {
|
||||
whirlpoolsConfigAddress = new PublicKey(
|
||||
"FcrweFY1G9HJAHG5inkGB6pKg1HZ6x9UC2WioAfWrGkR",
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported network");
|
||||
}
|
||||
const wallet = new Wallet(agent.wallet);
|
||||
const ctx = WhirlpoolContext.from(
|
||||
agent.connection,
|
||||
wallet,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
);
|
||||
const fetcher = ctx.fetcher;
|
||||
const client = buildWhirlpoolClient(ctx);
|
||||
|
||||
const correctTokenOrder = PoolUtil.orderMints(mintDeploy, mintPair).map(
|
||||
(addr) => addr.toString(),
|
||||
);
|
||||
const isCorrectMintOrder = correctTokenOrder[0] === mintDeploy.toString();
|
||||
let mintA;
|
||||
let mintB;
|
||||
if (!isCorrectMintOrder) {
|
||||
[mintA, mintB] = [mintPair, mintDeploy];
|
||||
initialPrice = new Decimal(1 / initialPrice.toNumber());
|
||||
} else {
|
||||
[mintA, mintB] = [mintDeploy, mintPair];
|
||||
}
|
||||
const mintAAccount = await fetcher.getMintInfo(mintA);
|
||||
const mintBAccount = await fetcher.getMintInfo(mintB);
|
||||
if (mintAAccount === null || mintBAccount === null) {
|
||||
throw Error("Mint account not found");
|
||||
}
|
||||
|
||||
const tickSpacing = FEE_TIERS[feeTier];
|
||||
const initialTick = PriceMath.priceToInitializableTickIndex(
|
||||
initialPrice,
|
||||
mintAAccount.decimals,
|
||||
mintBAccount.decimals,
|
||||
tickSpacing,
|
||||
);
|
||||
const { poolKey, tx: txBuilder } = await client.createPool(
|
||||
whirlpoolsConfigAddress,
|
||||
mintA,
|
||||
mintB,
|
||||
tickSpacing,
|
||||
initialTick,
|
||||
wallet.publicKey,
|
||||
);
|
||||
|
||||
const txPayload = await txBuilder.build();
|
||||
const txPayloadDecompiled = TransactionMessage.decompile(
|
||||
(txPayload.transaction as VersionedTransaction).message,
|
||||
);
|
||||
const instructions = txPayloadDecompiled.instructions;
|
||||
|
||||
const txId = await sendTx(
|
||||
agent,
|
||||
instructions,
|
||||
txPayload.signers as Keypair[],
|
||||
);
|
||||
return JSON.stringify({
|
||||
transactionId: txId,
|
||||
whirlpoolAddress: poolKey.toString(),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
422
src/tools/orca_create_single_sided_liquidity_pool.ts
Normal file
422
src/tools/orca_create_single_sided_liquidity_pool.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
import {
|
||||
Keypair,
|
||||
PublicKey,
|
||||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
import { BN, Wallet } from "@coral-xyz/anchor";
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
PDAUtil,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
WhirlpoolContext,
|
||||
TickUtil,
|
||||
PriceMath,
|
||||
PoolUtil,
|
||||
TokenExtensionContextForPool,
|
||||
NO_TOKEN_EXTENSION_CONTEXT,
|
||||
TokenExtensionUtil,
|
||||
WhirlpoolIx,
|
||||
IncreaseLiquidityQuoteParam,
|
||||
increaseLiquidityQuoteByInputTokenWithParams,
|
||||
} from "@orca-so/whirlpools-sdk";
|
||||
import {
|
||||
Percentage,
|
||||
resolveOrCreateATAs,
|
||||
TransactionBuilder,
|
||||
} from "@orca-so/common-sdk";
|
||||
import {
|
||||
increaseLiquidityIx,
|
||||
increaseLiquidityV2Ix,
|
||||
initTickArrayIx,
|
||||
openPositionWithTokenExtensionsIx,
|
||||
} from "@orca-so/whirlpools-sdk/dist/instructions";
|
||||
import {
|
||||
getAssociatedTokenAddressSync,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { sendTx } from "../utils/send_tx";
|
||||
|
||||
/**
|
||||
* Maps fee tier bps to their corresponding tick spacing values in the Orca Whirlpool protocol.
|
||||
*
|
||||
* @remarks
|
||||
* Fee tiers determine the percentage of fees collected on swaps, while tick spacing affects
|
||||
* the granularity of price ranges for liquidity positions.
|
||||
*
|
||||
* For more details, refer to:
|
||||
* - [Whirlpool Fees](https://orca-so.github.io/whirlpools/Architecture%20Overview/Whirlpool%20Fees)
|
||||
* - [Whirlpool Parameters](https://orca-so.github.io/whirlpools/Architecture%20Overview/Whirlpool%20Parameters)
|
||||
*
|
||||
* @example
|
||||
* const tickSpacing = FEE_TIERS[1]; // returns 1
|
||||
*/
|
||||
export const FEE_TIERS = {
|
||||
1: 1,
|
||||
2: 2,
|
||||
4: 4,
|
||||
5: 8,
|
||||
16: 16,
|
||||
30: 64,
|
||||
65: 96,
|
||||
100: 128,
|
||||
200: 256,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* # Creates a single-sided liquidity pool.
|
||||
*
|
||||
* This function initializes a new Whirlpool (liquidity pool) on Orca and seeds it with liquidity from a single token.
|
||||
*
|
||||
* ## Example Usage:
|
||||
* You created a new token called SHARK, and you want to set the initial price to 0.001 USDC.
|
||||
* You set `depositTokenMint` to SHARK's mint address and `otherTokenMint` to USDC's mint address.
|
||||
* You can minimize price impact for buyers in a few ways:
|
||||
* 1. Increase the amount of tokens you deposit
|
||||
* 2. Set the initial price very low
|
||||
* 3. Set the maximum price closer to the initial price
|
||||
*
|
||||
* ### Note for experts:
|
||||
* The Wrhirlpool program initializes the Whirlpool with the in a specific order. This might not be
|
||||
* the order you expect, so the function checks the order and adjusts the inverts the prices. This means that
|
||||
* on-chain the Whirlpool might be configured as USDC/SHARK instead of SHARK/USDC, and the on-chain price will
|
||||
* be 1/`initialPrice`. This will not affect the price of the token as you intended it to be.
|
||||
*
|
||||
* @param agent - The `SolanaAgentKit` instance representing the wallet and connection details.
|
||||
* @param depositTokenAmount - The amount of the deposit token to deposit in the pool.
|
||||
* @param depositTokenMint - The mint address of the token being deposited into the pool, eg. SHARK.
|
||||
* @param otherTokenMint - The mint address of the other token in the pool, eg. USDC.
|
||||
* @param initialPrice - The initial price of the deposit token in terms of the other token.
|
||||
* @param maxPrice - The maximum price at which liquidity is added.
|
||||
* @param feeTier - The fee tier bps for the pool, determining tick spacing and fee collection rates.
|
||||
*
|
||||
* @returns A promise that resolves to a transaction ID (`string`) of the transaction creating the pool.
|
||||
*
|
||||
* @throws Will throw an error if:
|
||||
* - Mint accounts for the tokens cannot be fetched.
|
||||
* - Prices are out of bounds.
|
||||
*
|
||||
* @remarks
|
||||
* This function is designed for single-sided deposits where users only contribute one type of token,
|
||||
* and the function manages mint order and necessary calculations.
|
||||
*/
|
||||
export async function orcaCreateSingleSidedLiquidityPool(
|
||||
agent: SolanaAgentKit,
|
||||
depositTokenAmount: number,
|
||||
depositTokenMint: PublicKey,
|
||||
otherTokenMint: PublicKey,
|
||||
initialPrice: Decimal,
|
||||
maxPrice: Decimal,
|
||||
feeTierBps: keyof typeof FEE_TIERS,
|
||||
): Promise<string> {
|
||||
try {
|
||||
let whirlpoolsConfigAddress: PublicKey;
|
||||
if (agent.connection.rpcEndpoint.includes("mainnet")) {
|
||||
whirlpoolsConfigAddress = new PublicKey(
|
||||
"2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ",
|
||||
);
|
||||
} else if (agent.connection.rpcEndpoint.includes("devnet")) {
|
||||
whirlpoolsConfigAddress = new PublicKey(
|
||||
"FcrweFY1G9HJAHG5inkGB6pKg1HZ6x9UC2WioAfWrGkR",
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported network");
|
||||
}
|
||||
const wallet = new Wallet(agent.wallet);
|
||||
const ctx = WhirlpoolContext.from(
|
||||
agent.connection,
|
||||
wallet,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
);
|
||||
const fetcher = ctx.fetcher;
|
||||
|
||||
const correctTokenOrder = PoolUtil.orderMints(
|
||||
otherTokenMint,
|
||||
depositTokenMint,
|
||||
).map((addr) => addr.toString());
|
||||
const isCorrectMintOrder =
|
||||
correctTokenOrder[0] === depositTokenMint.toString();
|
||||
let mintA, mintB;
|
||||
if (isCorrectMintOrder) {
|
||||
[mintA, mintB] = [depositTokenMint, otherTokenMint];
|
||||
} else {
|
||||
[mintA, mintB] = [otherTokenMint, depositTokenMint];
|
||||
initialPrice = new Decimal(1 / initialPrice.toNumber());
|
||||
maxPrice = new Decimal(1 / maxPrice.toNumber());
|
||||
}
|
||||
const mintAAccount = await fetcher.getMintInfo(mintA);
|
||||
const mintBAccount = await fetcher.getMintInfo(mintB);
|
||||
if (mintAAccount === null || mintBAccount === null) {
|
||||
throw Error("Mint account not found");
|
||||
}
|
||||
const tickSpacing = FEE_TIERS[feeTierBps];
|
||||
const tickIndex = PriceMath.priceToTickIndex(
|
||||
initialPrice,
|
||||
mintAAccount.decimals,
|
||||
mintBAccount.decimals,
|
||||
);
|
||||
const initialTick = TickUtil.getInitializableTickIndex(
|
||||
tickIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
|
||||
const tokenExtensionCtx: TokenExtensionContextForPool = {
|
||||
...NO_TOKEN_EXTENSION_CONTEXT,
|
||||
tokenMintWithProgramA: mintAAccount,
|
||||
tokenMintWithProgramB: mintBAccount,
|
||||
};
|
||||
const feeTierKey = PDAUtil.getFeeTier(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
whirlpoolsConfigAddress,
|
||||
tickSpacing,
|
||||
).publicKey;
|
||||
const initSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(initialTick);
|
||||
const tokenVaultAKeypair = Keypair.generate();
|
||||
const tokenVaultBKeypair = Keypair.generate();
|
||||
const whirlpoolPda = PDAUtil.getWhirlpool(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
whirlpoolsConfigAddress,
|
||||
mintA,
|
||||
mintB,
|
||||
FEE_TIERS[feeTierBps],
|
||||
);
|
||||
const tokenBadgeA = PDAUtil.getTokenBadge(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
whirlpoolsConfigAddress,
|
||||
mintA,
|
||||
).publicKey;
|
||||
const tokenBadgeB = PDAUtil.getTokenBadge(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
whirlpoolsConfigAddress,
|
||||
mintB,
|
||||
).publicKey;
|
||||
const baseParamsPool = {
|
||||
initSqrtPrice,
|
||||
whirlpoolsConfig: whirlpoolsConfigAddress,
|
||||
whirlpoolPda,
|
||||
tokenMintA: mintA,
|
||||
tokenMintB: mintB,
|
||||
tokenVaultAKeypair,
|
||||
tokenVaultBKeypair,
|
||||
feeTierKey,
|
||||
tickSpacing: tickSpacing,
|
||||
funder: wallet.publicKey,
|
||||
};
|
||||
const initPoolIx = !TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx)
|
||||
? WhirlpoolIx.initializePoolIx(ctx.program, baseParamsPool)
|
||||
: WhirlpoolIx.initializePoolV2Ix(ctx.program, {
|
||||
...baseParamsPool,
|
||||
tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram,
|
||||
tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram,
|
||||
tokenBadgeA,
|
||||
tokenBadgeB,
|
||||
});
|
||||
const initialTickArrayStartTick = TickUtil.getStartTickIndex(
|
||||
initialTick,
|
||||
tickSpacing,
|
||||
);
|
||||
const initialTickArrayPda = PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolPda.publicKey,
|
||||
initialTickArrayStartTick,
|
||||
);
|
||||
|
||||
const txBuilder = new TransactionBuilder(
|
||||
ctx.provider.connection,
|
||||
ctx.provider.wallet,
|
||||
ctx.txBuilderOpts,
|
||||
);
|
||||
txBuilder.addInstruction(initPoolIx);
|
||||
txBuilder.addInstruction(
|
||||
initTickArrayIx(ctx.program, {
|
||||
startTick: initialTickArrayStartTick,
|
||||
tickArrayPda: initialTickArrayPda,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
funder: wallet.publicKey,
|
||||
}),
|
||||
);
|
||||
|
||||
let tickLowerIndex, tickUpperIndex;
|
||||
if (isCorrectMintOrder) {
|
||||
tickLowerIndex = initialTick;
|
||||
tickUpperIndex = PriceMath.priceToTickIndex(
|
||||
maxPrice,
|
||||
mintAAccount.decimals,
|
||||
mintBAccount.decimals,
|
||||
);
|
||||
} else {
|
||||
tickLowerIndex = PriceMath.priceToTickIndex(
|
||||
maxPrice,
|
||||
mintAAccount.decimals,
|
||||
mintBAccount.decimals,
|
||||
);
|
||||
tickUpperIndex = initialTick;
|
||||
}
|
||||
const tickLowerInitializableIndex = TickUtil.getInitializableTickIndex(
|
||||
tickLowerIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
const tickUpperInitializableIndex = TickUtil.getInitializableTickIndex(
|
||||
tickUpperIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
if (
|
||||
!TickUtil.checkTickInBounds(tickLowerInitializableIndex) ||
|
||||
!TickUtil.checkTickInBounds(tickUpperInitializableIndex)
|
||||
) {
|
||||
throw Error("Prices out of bounds");
|
||||
}
|
||||
depositTokenAmount = isCorrectMintOrder
|
||||
? depositTokenAmount * Math.pow(10, mintAAccount.decimals)
|
||||
: depositTokenAmount * Math.pow(10, mintBAccount.decimals);
|
||||
const increasLiquidityQuoteParam: IncreaseLiquidityQuoteParam = {
|
||||
inputTokenAmount: new BN(depositTokenAmount),
|
||||
inputTokenMint: depositTokenMint,
|
||||
tokenMintA: mintA,
|
||||
tokenMintB: mintB,
|
||||
tickCurrentIndex: initialTick,
|
||||
sqrtPrice: initSqrtPrice,
|
||||
tickLowerIndex: tickLowerInitializableIndex,
|
||||
tickUpperIndex: tickUpperInitializableIndex,
|
||||
tokenExtensionCtx: tokenExtensionCtx,
|
||||
slippageTolerance: Percentage.fromFraction(0, 100),
|
||||
};
|
||||
const liquidityInput = increaseLiquidityQuoteByInputTokenWithParams(
|
||||
increasLiquidityQuoteParam,
|
||||
);
|
||||
const { liquidityAmount: liquidity, tokenMaxA, tokenMaxB } = liquidityInput;
|
||||
|
||||
const positionMintKeypair = Keypair.generate();
|
||||
const positionMintPubkey = positionMintKeypair.publicKey;
|
||||
const positionPda = PDAUtil.getPosition(
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
positionMintPubkey,
|
||||
);
|
||||
const positionTokenAccountAddress = getAssociatedTokenAddressSync(
|
||||
positionMintPubkey,
|
||||
wallet.publicKey,
|
||||
ctx.accountResolverOpts.allowPDAOwnerAddress,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
);
|
||||
const params = {
|
||||
funder: wallet.publicKey,
|
||||
owner: wallet.publicKey,
|
||||
positionPda,
|
||||
positionTokenAccount: positionTokenAccountAddress,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
tickLowerIndex: tickLowerInitializableIndex,
|
||||
tickUpperIndex: tickUpperInitializableIndex,
|
||||
};
|
||||
const positionIx = openPositionWithTokenExtensionsIx(ctx.program, {
|
||||
...params,
|
||||
positionMint: positionMintPubkey,
|
||||
withTokenMetadataExtension: true,
|
||||
});
|
||||
|
||||
txBuilder.addInstruction(positionIx);
|
||||
txBuilder.addSigner(positionMintKeypair);
|
||||
|
||||
const [ataA, ataB] = await resolveOrCreateATAs(
|
||||
ctx.connection,
|
||||
wallet.publicKey,
|
||||
[
|
||||
{ tokenMint: mintA, wrappedSolAmountIn: tokenMaxA },
|
||||
{ tokenMint: mintB, wrappedSolAmountIn: tokenMaxB },
|
||||
],
|
||||
() => ctx.fetcher.getAccountRentExempt(),
|
||||
wallet.publicKey,
|
||||
undefined,
|
||||
ctx.accountResolverOpts.allowPDAOwnerAddress,
|
||||
"ata",
|
||||
);
|
||||
const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA;
|
||||
const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = ataB;
|
||||
|
||||
txBuilder.addInstruction(tokenOwnerAccountAIx);
|
||||
txBuilder.addInstruction(tokenOwnerAccountBIx);
|
||||
|
||||
const tickArrayLowerStartIndex = TickUtil.getStartTickIndex(
|
||||
tickLowerInitializableIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
const tickArrayUpperStartIndex = TickUtil.getStartTickIndex(
|
||||
tickUpperInitializableIndex,
|
||||
tickSpacing,
|
||||
);
|
||||
const tickArrayLowerPda = PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolPda.publicKey,
|
||||
tickArrayLowerStartIndex,
|
||||
);
|
||||
const tickArrayUpperPda = PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolPda.publicKey,
|
||||
tickArrayUpperStartIndex,
|
||||
);
|
||||
if (tickArrayUpperStartIndex !== tickArrayLowerStartIndex) {
|
||||
if (isCorrectMintOrder) {
|
||||
txBuilder.addInstruction(
|
||||
initTickArrayIx(ctx.program, {
|
||||
startTick: tickArrayUpperStartIndex,
|
||||
tickArrayPda: tickArrayUpperPda,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
funder: wallet.publicKey,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
txBuilder.addInstruction(
|
||||
initTickArrayIx(ctx.program, {
|
||||
startTick: tickArrayLowerStartIndex,
|
||||
tickArrayPda: tickArrayLowerPda,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
funder: wallet.publicKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const baseParamsLiquidity = {
|
||||
liquidityAmount: liquidity,
|
||||
tokenMaxA,
|
||||
tokenMaxB,
|
||||
whirlpool: whirlpoolPda.publicKey,
|
||||
positionAuthority: wallet.publicKey,
|
||||
position: positionPda.publicKey,
|
||||
positionTokenAccount: positionTokenAccountAddress,
|
||||
tokenOwnerAccountA,
|
||||
tokenOwnerAccountB,
|
||||
tokenVaultA: tokenVaultAKeypair.publicKey,
|
||||
tokenVaultB: tokenVaultBKeypair.publicKey,
|
||||
tickArrayLower: tickArrayLowerPda.publicKey,
|
||||
tickArrayUpper: tickArrayUpperPda.publicKey,
|
||||
};
|
||||
|
||||
const liquidityIx = !TokenExtensionUtil.isV2IxRequiredPool(
|
||||
tokenExtensionCtx,
|
||||
)
|
||||
? increaseLiquidityIx(ctx.program, baseParamsLiquidity)
|
||||
: increaseLiquidityV2Ix(ctx.program, {
|
||||
...baseParamsLiquidity,
|
||||
tokenMintA: mintA,
|
||||
tokenMintB: mintB,
|
||||
tokenProgramA: tokenExtensionCtx.tokenMintWithProgramA.tokenProgram,
|
||||
tokenProgramB: tokenExtensionCtx.tokenMintWithProgramB.tokenProgram,
|
||||
});
|
||||
txBuilder.addInstruction(liquidityIx);
|
||||
|
||||
const txPayload = await txBuilder.build();
|
||||
const instructions = TransactionMessage.decompile(
|
||||
(txPayload.transaction as VersionedTransaction).message,
|
||||
).instructions;
|
||||
|
||||
const txId = await sendTx(agent, instructions, [
|
||||
positionMintKeypair,
|
||||
tokenVaultAKeypair,
|
||||
tokenVaultBKeypair,
|
||||
]);
|
||||
return txId;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send transaction: ${JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
121
src/tools/orca_fetch_positions.ts
Normal file
121
src/tools/orca_fetch_positions.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
import { Wallet } from "@coral-xyz/anchor";
|
||||
import {
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
WhirlpoolContext,
|
||||
buildWhirlpoolClient,
|
||||
getAllPositionAccountsByOwner,
|
||||
PriceMath,
|
||||
} from "@orca-so/whirlpools-sdk";
|
||||
|
||||
interface PositionInfo {
|
||||
whirlpoolAddress: string;
|
||||
positionInRange: boolean;
|
||||
distanceFromCenterBps: number;
|
||||
}
|
||||
|
||||
type PositionDataMap = {
|
||||
[positionMintAddress: string]: PositionInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Fetches Liquidity Position Data in Orca Whirlpools
|
||||
*
|
||||
* Fetches data for all liquidity positions owned by the provided wallet, including:
|
||||
* - Whirlpool address.
|
||||
* - Whether the position is in range.
|
||||
* - Distance from the center price to the current price in basis points.
|
||||
*
|
||||
* ## Parameters
|
||||
* - `agent`: The `SolanaAgentKit` instance representing the wallet and connection.
|
||||
*
|
||||
* ## Returns
|
||||
* A JSON string with an object mapping position mint addresses to position details:
|
||||
* ```json
|
||||
* {
|
||||
* "positionMintAddress1": {
|
||||
* "whirlpoolAddress": "whirlpoolAddress1",
|
||||
* "positionInRange": true,
|
||||
* "distanceFromCenterBps": 250
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Throws
|
||||
* - If positions cannot be fetched or processed.
|
||||
* - If the position mint address is invalid.
|
||||
*
|
||||
* @param agent - The `SolanaAgentKit` instance.
|
||||
* @returns A JSON string with position data.
|
||||
*/
|
||||
export async function orcaFetchPositions(
|
||||
agent: SolanaAgentKit,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const wallet = new Wallet(agent.wallet);
|
||||
const ctx = WhirlpoolContext.from(
|
||||
agent.connection,
|
||||
wallet,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
);
|
||||
const client = buildWhirlpoolClient(ctx);
|
||||
|
||||
const positions = await getAllPositionAccountsByOwner({
|
||||
ctx,
|
||||
owner: agent.wallet.publicKey,
|
||||
});
|
||||
const positionDatas = [
|
||||
...positions.positions.entries(),
|
||||
...positions.positionsWithTokenExtensions.entries(),
|
||||
];
|
||||
const result: PositionDataMap = {};
|
||||
for (const [, positionData] of positionDatas) {
|
||||
const positionMintAddress = positionData.positionMint;
|
||||
const whirlpoolAddress = positionData.whirlpool;
|
||||
const whirlpool = await client.getPool(whirlpoolAddress);
|
||||
const whirlpoolData = whirlpool.getData();
|
||||
const sqrtPrice = whirlpoolData.sqrtPrice;
|
||||
const currentTick = whirlpoolData.tickCurrentIndex;
|
||||
const mintA = whirlpool.getTokenAInfo();
|
||||
const mintB = whirlpool.getTokenBInfo();
|
||||
const currentPrice = PriceMath.sqrtPriceX64ToPrice(
|
||||
sqrtPrice,
|
||||
mintA.decimals,
|
||||
mintB.decimals,
|
||||
);
|
||||
const lowerTick = positionData.tickLowerIndex;
|
||||
const upperTick = positionData.tickUpperIndex;
|
||||
const lowerPrice = PriceMath.tickIndexToPrice(
|
||||
lowerTick,
|
||||
mintA.decimals,
|
||||
mintB.decimals,
|
||||
);
|
||||
const upperPrice = PriceMath.tickIndexToPrice(
|
||||
upperTick,
|
||||
mintA.decimals,
|
||||
mintB.decimals,
|
||||
);
|
||||
const centerPosition = lowerPrice.add(upperPrice).div(2);
|
||||
|
||||
const positionInRange =
|
||||
currentTick > lowerTick && currentTick < upperTick ? true : false;
|
||||
const distanceFromCenterBps = Math.ceil(
|
||||
currentPrice
|
||||
.sub(centerPosition)
|
||||
.abs()
|
||||
.div(centerPosition)
|
||||
.mul(10000)
|
||||
.toNumber(),
|
||||
);
|
||||
|
||||
result[positionMintAddress.toString()] = {
|
||||
whirlpoolAddress: whirlpoolAddress.toString(),
|
||||
positionInRange,
|
||||
distanceFromCenterBps,
|
||||
};
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
} catch (error) {
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
161
src/tools/orca_open_centered_position_with_liquidity.ts
Normal file
161
src/tools/orca_open_centered_position_with_liquidity.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
Keypair,
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
import { Wallet } from "@coral-xyz/anchor";
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
WhirlpoolContext,
|
||||
PriceMath,
|
||||
buildWhirlpoolClient,
|
||||
increaseLiquidityQuoteByInputToken,
|
||||
TokenExtensionContextForPool,
|
||||
NO_TOKEN_EXTENSION_CONTEXT,
|
||||
} from "@orca-so/whirlpools-sdk";
|
||||
|
||||
import { sendTx } from "../utils/send_tx";
|
||||
import { Percentage } from "@orca-so/common-sdk";
|
||||
import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
|
||||
|
||||
/**
|
||||
* # Opens a Centered Liquidity Position in an Orca Whirlpool
|
||||
*
|
||||
* This function opens a centered liquidity position in a specified Orca Whirlpool. The user defines
|
||||
* a basis point (bps) offset from the current price of the pool to set the lower and upper bounds of the position.
|
||||
* The user also specifies the token mint and the amount to deposit. The required amount of the other token
|
||||
* is calculated automatically.
|
||||
*
|
||||
* ## Parameters
|
||||
* - `agent`: The `SolanaAgentKit` instance representing the wallet and connection details.
|
||||
* - `whirlpoolAddress`: The address of the Orca Whirlpool where the position will be opened.
|
||||
* - `priceOffsetBps`: The basis point (bps) offset (on one side) from the current price fo the pool. For example,
|
||||
* 500 bps (5%) creates a range from 95% to 105% of the current pool price.
|
||||
* - `inputTokenMint`: The mint address of the token being deposited (e.g., USDC or another token).
|
||||
* - `inputAmount`: The amount of the input token to deposit, specified as a `Decimal` value.
|
||||
*
|
||||
* ## Returns
|
||||
* A `Promise` that resolves to the transaction ID (`string`) of the transaction that opens the position.
|
||||
*
|
||||
* ## Notes
|
||||
* - The `priceOffsetBps` specifies the range symmetrically around the current price.
|
||||
* - The specified `inputTokenMint` determines which token is deposited directly. The function calculates
|
||||
* the required amount of the other token based on the specified price range.
|
||||
* - This function supports Orca's token extensions for managing tokens with special behaviors.
|
||||
* - The function assumes a maximum slippage of 1% for liquidity provision.
|
||||
*
|
||||
* ## Throws
|
||||
* An error will be thrown if:
|
||||
* - The specified Whirlpool address is invalid or inaccessible.
|
||||
* - The transaction fails to send.
|
||||
* - Any required mint information cannot be fetched.
|
||||
*
|
||||
* @param agent - The `SolanaAgentKit` instance representing the wallet and connection.
|
||||
* @param whirlpoolAddress - The address of the Orca Whirlpool.
|
||||
* @param priceOffsetBps - The basis point offset (one side) from the current pool price.
|
||||
* @param inputTokenMint - The mint address of the token to deposit.
|
||||
* @param inputAmount - The amount of the input token to deposit.
|
||||
* @returns A promise resolving to the transaction ID (`string`).
|
||||
*/
|
||||
export async function orcaOpenCenteredPositionWithLiquidity(
|
||||
agent: SolanaAgentKit,
|
||||
whirlpoolAddress: PublicKey,
|
||||
priceOffsetBps: number,
|
||||
inputTokenMint: PublicKey,
|
||||
inputAmount: Decimal,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const wallet = new Wallet(agent.wallet);
|
||||
const ctx = WhirlpoolContext.from(
|
||||
agent.connection,
|
||||
wallet,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
);
|
||||
const client = buildWhirlpoolClient(ctx);
|
||||
|
||||
const whirlpool = await client.getPool(whirlpoolAddress);
|
||||
const whirlpoolData = whirlpool.getData();
|
||||
const mintInfoA = whirlpool.getTokenAInfo();
|
||||
const mintInfoB = whirlpool.getTokenBInfo();
|
||||
const price = PriceMath.sqrtPriceX64ToPrice(
|
||||
whirlpoolData.sqrtPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
);
|
||||
|
||||
const lowerPrice = price.mul(1 - priceOffsetBps / 10000);
|
||||
const upperPrice = price.mul(1 + priceOffsetBps / 10000);
|
||||
const lowerTick = PriceMath.priceToInitializableTickIndex(
|
||||
lowerPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
whirlpoolData.tickSpacing,
|
||||
);
|
||||
const upperTick = PriceMath.priceToInitializableTickIndex(
|
||||
upperPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
whirlpoolData.tickSpacing,
|
||||
);
|
||||
|
||||
const txBuilderTickArrays = await whirlpool.initTickArrayForTicks([
|
||||
lowerTick,
|
||||
upperTick,
|
||||
]);
|
||||
let instructions: TransactionInstruction[] = [];
|
||||
let signers: Keypair[] = [];
|
||||
if (txBuilderTickArrays !== null) {
|
||||
const txPayloadTickArrays = await txBuilderTickArrays.build();
|
||||
const txPayloadTickArraysDecompiled = TransactionMessage.decompile(
|
||||
(txPayloadTickArrays.transaction as VersionedTransaction).message,
|
||||
);
|
||||
const instructionsTickArrays = txPayloadTickArraysDecompiled.instructions;
|
||||
instructions = instructions.concat(instructionsTickArrays);
|
||||
signers = signers.concat(txPayloadTickArrays.signers as Keypair[]);
|
||||
}
|
||||
|
||||
const tokenExtensionCtx: TokenExtensionContextForPool = {
|
||||
...NO_TOKEN_EXTENSION_CONTEXT,
|
||||
tokenMintWithProgramA: mintInfoA,
|
||||
tokenMintWithProgramB: mintInfoB,
|
||||
};
|
||||
const increaseLiquiditQuote = increaseLiquidityQuoteByInputToken(
|
||||
inputTokenMint,
|
||||
inputAmount,
|
||||
lowerTick,
|
||||
upperTick,
|
||||
Percentage.fromFraction(1, 100),
|
||||
whirlpool,
|
||||
tokenExtensionCtx,
|
||||
);
|
||||
const { positionMint, tx: txBuilder } =
|
||||
await whirlpool.openPositionWithMetadata(
|
||||
lowerTick,
|
||||
upperTick,
|
||||
increaseLiquiditQuote,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
);
|
||||
|
||||
const txPayload = await txBuilder.build();
|
||||
const txPayloadDecompiled = TransactionMessage.decompile(
|
||||
(txPayload.transaction as VersionedTransaction).message,
|
||||
);
|
||||
instructions = instructions.concat(txPayloadDecompiled.instructions);
|
||||
signers = signers.concat(txPayload.signers as Keypair[]);
|
||||
|
||||
const txId = await sendTx(agent, instructions, signers);
|
||||
return JSON.stringify({
|
||||
transactionId: txId,
|
||||
positionMint: positionMint.toString(),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
177
src/tools/orca_open_single_sided_position.ts
Normal file
177
src/tools/orca_open_single_sided_position.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
Keypair,
|
||||
PublicKey,
|
||||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
import { Wallet } from "@coral-xyz/anchor";
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
WhirlpoolContext,
|
||||
PriceMath,
|
||||
buildWhirlpoolClient,
|
||||
increaseLiquidityQuoteByInputToken,
|
||||
TokenExtensionContextForPool,
|
||||
NO_TOKEN_EXTENSION_CONTEXT,
|
||||
} from "@orca-so/whirlpools-sdk";
|
||||
import { sendTx } from "../utils/send_tx";
|
||||
import { Percentage } from "@orca-so/common-sdk";
|
||||
import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
|
||||
|
||||
/**
|
||||
* # Opens a Single-Sided Liquidity Position in an Orca Whirlpool
|
||||
*
|
||||
* This function opens a single-sided liquidity position in a specified Orca Whirlpool. The user specifies
|
||||
* a basis point (bps) offset from the current price for the lower bound and a width (bps) for the range width.
|
||||
* The required amount of the other token is calculated automatically.
|
||||
*
|
||||
* ## Parameters
|
||||
* - `agent`: The `SolanaAgentKit` instance representing the wallet and connection details.
|
||||
* - `whirlpoolAddress`: The address of the Orca Whirlpool where the position will be opened.
|
||||
* - `distanceFromCurrentPriceBps`: The basis point offset from the current price for the lower bound.
|
||||
* - `widthBps`: The width of the range as a percentage increment from the lower bound.
|
||||
* - `inputTokenMint`: The mint address of the token being deposited (e.g., USDC or another token).
|
||||
* - `inputAmount`: The amount of the input token to deposit, specified as a `Decimal` value.
|
||||
*
|
||||
* ## Returns
|
||||
* A `Promise` that resolves to the transaction ID (`string`) of the transaction that opens the position.
|
||||
*
|
||||
* ## Notes
|
||||
* - The `distanceFromCurrentPriceBps` specifies the starting point of the range.
|
||||
* - The `widthBps` determines the range size from the lower bound.
|
||||
* - The specified `inputTokenMint` determines which token is deposited directly.
|
||||
*
|
||||
* @param agent - The `SolanaAgentKit` instance representing the wallet and connection.
|
||||
* @param whirlpoolAddress - The address of the Orca Whirlpool.
|
||||
* @param distanceFromCurrentPriceBps - The basis point offset from the current price for the lower bound.
|
||||
* @param widthBps - The width of the range as a percentage increment from the lower bound.
|
||||
* @param inputTokenMint - The mint address of the token to deposit.
|
||||
* @param inputAmount - The amount of the input token to deposit.
|
||||
* @returns A promise resolving to the transaction ID (`string`).
|
||||
*/
|
||||
export async function orcaOpenSingleSidedPosition(
|
||||
agent: SolanaAgentKit,
|
||||
whirlpoolAddress: PublicKey,
|
||||
distanceFromCurrentPriceBps: number,
|
||||
widthBps: number,
|
||||
inputTokenMint: PublicKey,
|
||||
inputAmount: Decimal,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const wallet = new Wallet(agent.wallet);
|
||||
const ctx = WhirlpoolContext.from(
|
||||
agent.connection,
|
||||
wallet,
|
||||
ORCA_WHIRLPOOL_PROGRAM_ID,
|
||||
);
|
||||
const client = buildWhirlpoolClient(ctx);
|
||||
|
||||
const whirlpool = await client.getPool(whirlpoolAddress);
|
||||
const whirlpoolData = whirlpool.getData();
|
||||
const mintInfoA = whirlpool.getTokenAInfo();
|
||||
const mintInfoB = whirlpool.getTokenBInfo();
|
||||
const price = PriceMath.sqrtPriceX64ToPrice(
|
||||
whirlpoolData.sqrtPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
);
|
||||
|
||||
const isTokenA = inputTokenMint.equals(mintInfoA.mint);
|
||||
let lowerBoundPrice;
|
||||
let upperBoundPrice;
|
||||
let lowerTick;
|
||||
let upperTick;
|
||||
if (isTokenA) {
|
||||
lowerBoundPrice = price.mul(1 + distanceFromCurrentPriceBps / 10000);
|
||||
upperBoundPrice = lowerBoundPrice.mul(1 + widthBps / 10000);
|
||||
upperTick = PriceMath.priceToInitializableTickIndex(
|
||||
upperBoundPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
whirlpoolData.tickSpacing,
|
||||
);
|
||||
lowerTick = PriceMath.priceToInitializableTickIndex(
|
||||
lowerBoundPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
whirlpoolData.tickSpacing,
|
||||
);
|
||||
} else {
|
||||
lowerBoundPrice = price.mul(1 - distanceFromCurrentPriceBps / 10000);
|
||||
upperBoundPrice = lowerBoundPrice.mul(1 - widthBps / 10000);
|
||||
lowerTick = PriceMath.priceToInitializableTickIndex(
|
||||
upperBoundPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
whirlpoolData.tickSpacing,
|
||||
);
|
||||
upperTick = PriceMath.priceToInitializableTickIndex(
|
||||
lowerBoundPrice,
|
||||
mintInfoA.decimals,
|
||||
mintInfoB.decimals,
|
||||
whirlpoolData.tickSpacing,
|
||||
);
|
||||
}
|
||||
|
||||
const txBuilderTickArrays = await whirlpool.initTickArrayForTicks([
|
||||
lowerTick,
|
||||
upperTick,
|
||||
]);
|
||||
let txIds: string = "";
|
||||
if (txBuilderTickArrays !== null) {
|
||||
const txPayloadTickArrays = await txBuilderTickArrays.build();
|
||||
const txPayloadTickArraysDecompiled = TransactionMessage.decompile(
|
||||
(txPayloadTickArrays.transaction as VersionedTransaction).message,
|
||||
);
|
||||
const instructions = txPayloadTickArraysDecompiled.instructions;
|
||||
const signers = txPayloadTickArrays.signers as Keypair[];
|
||||
|
||||
const tickArrayTxId = await sendTx(agent, instructions, signers);
|
||||
txIds += tickArrayTxId + ",";
|
||||
}
|
||||
|
||||
const tokenExtensionCtx: TokenExtensionContextForPool = {
|
||||
...NO_TOKEN_EXTENSION_CONTEXT,
|
||||
tokenMintWithProgramA: mintInfoA,
|
||||
tokenMintWithProgramB: mintInfoB,
|
||||
};
|
||||
const increaseLiquiditQuote = increaseLiquidityQuoteByInputToken(
|
||||
inputTokenMint,
|
||||
inputAmount,
|
||||
lowerTick,
|
||||
upperTick,
|
||||
Percentage.fromFraction(1, 100),
|
||||
whirlpool,
|
||||
tokenExtensionCtx,
|
||||
);
|
||||
const { positionMint, tx: txBuilder } =
|
||||
await whirlpool.openPositionWithMetadata(
|
||||
lowerTick,
|
||||
upperTick,
|
||||
increaseLiquiditQuote,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
TOKEN_2022_PROGRAM_ID,
|
||||
);
|
||||
|
||||
const txPayload = await txBuilder.build();
|
||||
const txPayloadDecompiled = TransactionMessage.decompile(
|
||||
(txPayload.transaction as VersionedTransaction).message,
|
||||
);
|
||||
const instructions = txPayloadDecompiled.instructions;
|
||||
const signers = txPayload.signers as Keypair[];
|
||||
|
||||
const positionTxId = await sendTx(agent, instructions, signers);
|
||||
txIds += positionTxId;
|
||||
|
||||
return JSON.stringify({
|
||||
transactionIds: txIds,
|
||||
positionMint: positionMint.toString(),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,91 @@
|
||||
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
|
||||
import BN from "bn.js";
|
||||
import { PythPriceFeedIDItem } from "../types";
|
||||
|
||||
/**
|
||||
* Fetch the price feed ID for a given token symbol from Pyth
|
||||
* @param tokenSymbol Token symbol
|
||||
* @returns Price feed ID
|
||||
*/
|
||||
export async function fetchPythPriceFeedID(
|
||||
tokenSymbol: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const stableHermesServiceUrl: string = "https://hermes.pyth.network";
|
||||
|
||||
const response = await fetch(
|
||||
`${stableHermesServiceUrl}/v2/price_feeds?query=${tokenSymbol}&asset_type=crypto`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
throw new Error(`No price feed found for ${tokenSymbol}`);
|
||||
}
|
||||
|
||||
if (data.length > 1) {
|
||||
const filteredData = data.filter(
|
||||
(item: PythPriceFeedIDItem) =>
|
||||
item.attributes.base.toLowerCase() === tokenSymbol.toLowerCase(),
|
||||
);
|
||||
|
||||
if (filteredData.length === 0) {
|
||||
throw new Error(`No price feed found for ${tokenSymbol}`);
|
||||
}
|
||||
|
||||
return filteredData[0].id;
|
||||
}
|
||||
|
||||
return data[0].id;
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Fetching price feed ID from Pyth failed: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the price of a given price feed from Pyth
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param priceFeedID Price feed ID
|
||||
* @returns Latest price value from feed
|
||||
*
|
||||
* You can find priceFeedIDs here: https://www.pyth.network/developers/price-feed-ids#stable
|
||||
*/
|
||||
export async function pythFetchPrice(priceFeedID: string): Promise<string> {
|
||||
// get Hermes service URL from https://docs.pyth.network/price-feeds/api-instances-and-providers/hermes
|
||||
const stableHermesServiceUrl: string = "https://hermes.pyth.network";
|
||||
const connection = new PriceServiceConnection(stableHermesServiceUrl);
|
||||
const feeds = [priceFeedID];
|
||||
|
||||
export async function fetchPythPrice(feedID: string): Promise<string> {
|
||||
try {
|
||||
const currentPrice = await connection.getLatestPriceFeeds(feeds);
|
||||
const stableHermesServiceUrl: string = "https://hermes.pyth.network";
|
||||
|
||||
if (currentPrice === undefined) {
|
||||
throw new Error("Price data not available for the given token.");
|
||||
const response = await fetch(
|
||||
`${stableHermesServiceUrl}/v2/updates/price/latest?ids[]=${feedID}`,
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const parsedData = data.parsed;
|
||||
|
||||
if (parsedData.length === 0) {
|
||||
throw new Error(`No price data found for ${feedID}`);
|
||||
}
|
||||
|
||||
if (currentPrice.length === 0) {
|
||||
throw new Error("Price data not available for the given token.");
|
||||
const price = new BN(parsedData[0].price.price);
|
||||
const exponent = parsedData[0].price.expo;
|
||||
|
||||
if (exponent < 0) {
|
||||
const adjustedPrice = price.mul(new BN(100));
|
||||
const divisor = new BN(10).pow(new BN(-exponent));
|
||||
const scaledPrice = adjustedPrice.div(divisor);
|
||||
|
||||
const priceStr = scaledPrice.toString();
|
||||
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
|
||||
return formattedPrice.startsWith(".")
|
||||
? `0${formattedPrice}`
|
||||
: formattedPrice;
|
||||
}
|
||||
|
||||
// get price and exponent from price feed
|
||||
const price = new BN(currentPrice[0].getPriceUnchecked().price);
|
||||
const exponent = new BN(currentPrice[0].getPriceUnchecked().expo);
|
||||
|
||||
// convert to scaled price
|
||||
const scaledPrice = price.div(new BN(10).pow(exponent));
|
||||
|
||||
const scaledPrice = price.div(new BN(10).pow(new BN(exponent)));
|
||||
return scaledPrice.toString();
|
||||
} catch (error: any) {
|
||||
throw new Error(`Fetching price from Pyth failed: ${error.message}`);
|
||||
|
||||
@@ -24,7 +24,8 @@ export async function resolveSolDomain(
|
||||
|
||||
try {
|
||||
return await resolve(agent.connection, domain);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
throw new Error(`Failed to resolve domain: ${domain}`);
|
||||
}
|
||||
}
|
||||
|
||||
53
src/tools/rugcheck.ts
Normal file
53
src/tools/rugcheck.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { TokenCheck } from "../types";
|
||||
|
||||
const BASE_URL = "https://api.rugcheck.xyz/v1";
|
||||
|
||||
/**
|
||||
* Fetches a summary report for a specific token.
|
||||
* @async
|
||||
* @param {string} mint - The mint address of the token.
|
||||
* @returns {Promise<TokenCheck>} The token summary report.
|
||||
* @throws {Error} If the API call fails.
|
||||
*/
|
||||
export async function fetchTokenReportSummary(
|
||||
mint: string,
|
||||
): Promise<TokenCheck> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/tokens/${mint}/report/summary`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`Error fetching report summary for token ${mint}:`,
|
||||
error.message,
|
||||
);
|
||||
throw new Error(`Failed to fetch report summary for token ${mint}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a detailed report for a specific token.
|
||||
* @async
|
||||
* @param {string} mint - The mint address of the token.
|
||||
* @returns {Promise<TokenCheck>} The detailed token report.
|
||||
* @throws {Error} If the API call fails.
|
||||
*/
|
||||
export async function fetchTokenDetailedReport(
|
||||
mint: string,
|
||||
): Promise<TokenCheck> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/tokens/${mint}/report`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`Error fetching detailed report for token ${mint}:`,
|
||||
error.message,
|
||||
);
|
||||
throw new Error(`Failed to fetch detailed report for token ${mint}.`);
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ export async function sendCompressedAirdrop(
|
||||
agent.wallet.publicKey,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(
|
||||
"Source token account not found and failed to create it. Please add funds to your wallet and try again.",
|
||||
);
|
||||
|
||||
64
src/tools/stake_with_solayer.ts
Normal file
64
src/tools/stake_with_solayer.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { VersionedTransaction } from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
|
||||
/**
|
||||
* Stake SOL with Solayer
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param amount Amount of SOL to stake
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
export async function stakeWithSolayer(
|
||||
agent: SolanaAgentKit,
|
||||
amount: number,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://app.solayer.org/api/action/restake/ssol?amount=${amount}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
account: agent.wallet.publicKey.toBase58(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || "Staking request failed");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Deserialize and prepare transaction
|
||||
const txn = VersionedTransaction.deserialize(
|
||||
Buffer.from(data.transaction, "base64"),
|
||||
);
|
||||
|
||||
// Update blockhash
|
||||
const { blockhash } = await agent.connection.getLatestBlockhash();
|
||||
txn.message.recentBlockhash = blockhash;
|
||||
|
||||
// Sign and send transaction
|
||||
txn.sign([agent.wallet]);
|
||||
const signature = await agent.connection.sendTransaction(txn, {
|
||||
preflightCommitment: "confirmed",
|
||||
maxRetries: 3,
|
||||
});
|
||||
|
||||
// Wait for confirmation
|
||||
const latestBlockhash = await agent.connection.getLatestBlockhash();
|
||||
await agent.connection.confirmTransaction({
|
||||
signature,
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||
});
|
||||
|
||||
return signature;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
throw new Error(`Solayer sSOL staking failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
109
src/tools/tensor_trade.ts
Normal file
109
src/tools/tensor_trade.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import { TensorSwapSDK } from "@tensor-oss/tensorswap-sdk";
|
||||
import { PublicKey, Transaction } from "@solana/web3.js";
|
||||
import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
|
||||
import { BN } from "bn.js";
|
||||
import {
|
||||
getAssociatedTokenAddress,
|
||||
TOKEN_PROGRAM_ID,
|
||||
getAccount,
|
||||
} from "@solana/spl-token";
|
||||
|
||||
export async function listNFTForSale(
|
||||
agent: SolanaAgentKit,
|
||||
nftMint: PublicKey,
|
||||
price: number,
|
||||
): Promise<string> {
|
||||
try {
|
||||
if (!PublicKey.isOnCurve(nftMint)) {
|
||||
throw new Error("Invalid NFT mint address");
|
||||
}
|
||||
|
||||
const mintInfo = await agent.connection.getAccountInfo(nftMint);
|
||||
if (!mintInfo) {
|
||||
throw new Error(`NFT mint ${nftMint.toString()} does not exist`);
|
||||
}
|
||||
|
||||
const ata = await getAssociatedTokenAddress(nftMint, agent.wallet_address);
|
||||
|
||||
try {
|
||||
const tokenAccount = await getAccount(agent.connection, ata);
|
||||
|
||||
if (!tokenAccount || tokenAccount.amount <= 0) {
|
||||
throw new Error(`You don't own this NFT (${nftMint.toString()})`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
throw new Error(
|
||||
`No token account found for mint ${nftMint.toString()}. Make sure you own this NFT.`,
|
||||
);
|
||||
}
|
||||
|
||||
const provider = new AnchorProvider(
|
||||
agent.connection,
|
||||
new Wallet(agent.wallet),
|
||||
AnchorProvider.defaultOptions(),
|
||||
);
|
||||
|
||||
const tensorSwapSdk = new TensorSwapSDK({ provider });
|
||||
const priceInLamports = new BN(price * 1e9);
|
||||
const nftSource = await getAssociatedTokenAddress(
|
||||
nftMint,
|
||||
agent.wallet_address,
|
||||
);
|
||||
|
||||
const { tx } = await tensorSwapSdk.list({
|
||||
nftMint,
|
||||
nftSource,
|
||||
owner: agent.wallet_address,
|
||||
price: priceInLamports,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
payer: agent.wallet_address,
|
||||
});
|
||||
|
||||
const transaction = new Transaction();
|
||||
transaction.add(...tx.ixs);
|
||||
return await agent.connection.sendTransaction(transaction, [
|
||||
agent.wallet,
|
||||
...tx.extraSigners,
|
||||
]);
|
||||
} catch (error: any) {
|
||||
console.error("Full error details:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelListing(
|
||||
agent: SolanaAgentKit,
|
||||
nftMint: PublicKey,
|
||||
): Promise<string> {
|
||||
const provider = new AnchorProvider(
|
||||
agent.connection,
|
||||
new Wallet(agent.wallet),
|
||||
AnchorProvider.defaultOptions(),
|
||||
);
|
||||
|
||||
const tensorSwapSdk = new TensorSwapSDK({ provider });
|
||||
const nftDest = await getAssociatedTokenAddress(
|
||||
nftMint,
|
||||
agent.wallet_address,
|
||||
false,
|
||||
TOKEN_PROGRAM_ID,
|
||||
);
|
||||
|
||||
const { tx } = await tensorSwapSdk.delist({
|
||||
nftMint,
|
||||
nftDest,
|
||||
owner: agent.wallet_address,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
payer: agent.wallet_address,
|
||||
authData: null,
|
||||
});
|
||||
|
||||
const transaction = new Transaction();
|
||||
transaction.add(...tx.ixs);
|
||||
return await agent.connection.sendTransaction(transaction, [
|
||||
agent.wallet,
|
||||
...tx.extraSigners,
|
||||
]);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { VersionedTransaction, PublicKey } from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import { TOKENS, DEFAULT_OPTIONS, JUP_API } from "../constants";
|
||||
import {
|
||||
TOKENS,
|
||||
DEFAULT_OPTIONS,
|
||||
JUP_API,
|
||||
JUP_REFERRAL_ADDRESS,
|
||||
} from "../constants";
|
||||
import { getMint } from "@solana/spl-token";
|
||||
/**
|
||||
* Swap tokens using Jupiter Exchange
|
||||
@@ -11,6 +16,7 @@ import { getMint } from "@solana/spl-token";
|
||||
* @param slippageBps Slippage tolerance in basis points (default: 300 = 3%)
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
|
||||
export async function trade(
|
||||
agent: SolanaAgentKit,
|
||||
outputMint: PublicKey,
|
||||
@@ -38,11 +44,24 @@ export async function trade(
|
||||
`&amount=${scaledAmount}` +
|
||||
`&slippageBps=${slippageBps}` +
|
||||
`&onlyDirectRoutes=true` +
|
||||
`&maxAccounts=20`,
|
||||
`&maxAccounts=20` +
|
||||
`${agent.config.JUPITER_FEE_BPS ? `&platformFeeBps=${agent.config.JUPITER_FEE_BPS}` : ""}`,
|
||||
)
|
||||
).json();
|
||||
|
||||
// Get serialized transaction
|
||||
let feeAccount;
|
||||
if (agent.config.JUPITER_REFERRAL_ACCOUNT) {
|
||||
[feeAccount] = PublicKey.findProgramAddressSync(
|
||||
[
|
||||
Buffer.from("referral_ata"),
|
||||
new PublicKey(agent.config.JUPITER_REFERRAL_ACCOUNT).toBuffer(),
|
||||
TOKENS.SOL.toBuffer(),
|
||||
],
|
||||
new PublicKey(JUP_REFERRAL_ADDRESS),
|
||||
);
|
||||
}
|
||||
|
||||
const { swapTransaction } = await (
|
||||
await fetch("https://quote-api.jup.ag/v6/swap", {
|
||||
method: "POST",
|
||||
@@ -55,6 +74,7 @@ export async function trade(
|
||||
wrapAndUnwrapSol: true,
|
||||
dynamicComputeUnitLimit: true,
|
||||
prioritizationFeeLamports: "auto",
|
||||
feeAccount: feeAccount ? feeAccount.toString() : null,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
37
src/tools/withdraw_all.ts
Normal file
37
src/tools/withdraw_all.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
PublicKey,
|
||||
sendAndConfirmTransaction,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import { ManifestClient } from "@cks-systems/manifest-sdk";
|
||||
|
||||
/**
|
||||
* Withdraws all funds from Manifest
|
||||
* @param agent SolanaAgentKit instance
|
||||
* @param marketId Public key for the manifest market
|
||||
* @returns Transaction signature
|
||||
*/
|
||||
export async function withdrawAll(
|
||||
agent: SolanaAgentKit,
|
||||
marketId: PublicKey,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const mfxClient = await ManifestClient.getClientForMarket(
|
||||
agent.connection,
|
||||
marketId,
|
||||
agent.wallet,
|
||||
);
|
||||
|
||||
const withdrawAllIx = await mfxClient.withdrawAllIx();
|
||||
const signature = await sendAndConfirmTransaction(
|
||||
agent.connection,
|
||||
new Transaction().add(...withdrawAllIx),
|
||||
[agent.wallet],
|
||||
);
|
||||
|
||||
return signature;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Withdraw all failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user