Merge remote-tracking branch 'upstream/main' into jup-referral-fee

This commit is contained in:
YCrydev
2024-12-31 11:33:18 +01:00
64 changed files with 7080 additions and 1288 deletions

View File

@@ -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");
}
}

View File

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

View File

@@ -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}`);
}

View File

@@ -0,0 +1,50 @@
import {
LAMPORTS_PER_SOL,
ParsedAccountData,
PublicKey,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
/**
* Get the balance of SOL or an SPL token for the specified wallet address (other than the agent's wallet)
* @param agent - SolanaAgentKit instance
* @param wallet_address - Public key of the wallet to check balance for
* @param token_address - Optional SPL token mint address. If not provided, returns SOL balance
* @returns Promise resolving to the balance as a number (in UI units) or 0 if account doesn't exist
*/
export async function get_balance_other(
agent: SolanaAgentKit,
wallet_address: PublicKey,
token_address?: PublicKey,
): Promise<number> {
try {
if (!token_address) {
return (
(await agent.connection.getBalance(wallet_address)) / LAMPORTS_PER_SOL
);
}
const tokenAccounts = await agent.connection.getTokenAccountsByOwner(
wallet_address,
{ mint: token_address },
);
if (tokenAccounts.value.length === 0) {
console.warn(
`No token accounts found for wallet ${wallet_address.toString()} and token ${token_address.toString()}`,
);
return 0;
}
const tokenAccount = await agent.connection.getParsedAccountInfo(
tokenAccounts.value[0].pubkey,
);
const tokenData = tokenAccount.value?.data as ParsedAccountData;
return tokenData.parsed?.info?.tokenAmount?.uiAmount || 0;
} catch (error) {
throw new Error(
`Error fetching on-chain balance for ${token_address?.toString()}: ${error}`,
);
}
}

View File

@@ -2,6 +2,7 @@ export * from "./request_faucet_funds";
export * from "./deploy_token";
export * from "./deploy_collection";
export * from "./get_balance";
export * from "./get_balance_other";
export * from "./mint_nft";
export * from "./transfer";
export * from "./trade";
@@ -15,7 +16,12 @@ 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 "./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 "./get_all_domains_tlds";
export * from "./get_all_registered_all_domains";
export * from "./get_owned_domains_for_tld";
@@ -40,3 +46,5 @@ export * from "./create_gibwork_task";
export * from "./rock_paper_scissor";
export * from "./create_tiplinks";
export * from "./tensor_trade";

View 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 Orcas 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}`);
}
}

View 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}`);
}
}

View 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)}`);
}
}

View 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}`);
}
}

View 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 cuurent 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}`);
}
}

View 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}`);
}
}

108
src/tools/tensor_trade.ts Normal file
View File

@@ -0,0 +1,108 @@
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 (e) {
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,
]);
}

View File

@@ -1,19 +1,17 @@
import {
VersionedTransaction,
PublicKey,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import { VersionedTransaction, PublicKey } from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
import {
TOKENS,
DEFAULT_OPTIONS,
JUP_API,
JUP_REFERRAL_ADDRESS,
} from "../constants";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
import { getMint } from "@solana/spl-token";
/**
* Swap tokens using Jupiter Exchange
* @param agent SolanaAgentKit instance
@@ -40,12 +38,23 @@ export async function trade(
slippageBps: number = DEFAULT_OPTIONS.SLIPPAGE_BPS,
): Promise<string> {
try {
// Check if input token is native SOL
const isNativeSol = inputMint.equals(TOKENS.SOL);
// For native SOL, we use LAMPORTS_PER_SOL, otherwise fetch mint info
const inputDecimals = isNativeSol
? 9 // SOL always has 9 decimals
: (await getMint(agent.connection, inputMint)).decimals;
// Calculate the correct amount based on actual decimals
const scaledAmount = inputAmount * Math.pow(10, inputDecimals);
const quoteResponse = await (
await fetch(
`${JUP_API}/quote?` +
`inputMint=${inputMint.toString()}` +
`inputMint=${isNativeSol ? TOKENS.SOL.toString() : inputMint.toString()}` +
`&outputMint=${outputMint.toString()}` +
`&amount=${inputAmount * LAMPORTS_PER_SOL}` +
`&amount=${scaledAmount}` +
`&slippageBps=${slippageBps}` +
`&onlyDirectRoutes=true` +
`&maxAccounts=20` +