mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-14 07:26:46 +00:00
Update send transaction and orca tool (#59)
This PR adds the following 3 things: - Single Sided pool creation can now be done on Devnet - Changed the wrapping strategy for SOL to reduce transaction size in case the agent does not have WSOL account - Update send transaction logic. I noticed that Orca's tool is the only tool that utilizes the `sendTx` function, so I took the liberty to update the function to optimize it for transaction landing, which is especially important in DeFi operations, including: - Include computeBugetSetUnitLimit instruction - Replace the deprecated 'confirmTransaction' API method: https://solana.com/docs/rpc/deprecated/confirmtransaction - Client-side retry mechanism. https://www.helius.dev/blog/how-to-land-transactions-on-solana Example transaction executed by the agent: https://solscan.io/tx/3u59wSqNBGJqKJjN4n9vzhtx1w637wVDDhhsmBmtE1S8hgv1XRLPTbcrahot48Ya9YSZFcYqQHu3f9H87ssE8JNu?cluster=devnet
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const fetchCache = 'force-no-store';
|
||||
export const dynamic = "force-dynamic";
|
||||
export const fetchCache = "force-no-store";
|
||||
export const maxDuration = 300;
|
||||
|
||||
import { Bot, webhookCallback } from 'grammy';
|
||||
import { Bot, webhookCallback } from "grammy";
|
||||
import { SolanaAgentKit, createSolanaTools } from "solana-agent-kit";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
@@ -10,10 +10,11 @@ import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { HumanMessage } from "@langchain/core/messages";
|
||||
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
if (!token) throw new Error('TELEGRAM_BOT_TOKEN environment variable not found.');
|
||||
if (!token) {
|
||||
throw new Error("TELEGRAM_BOT_TOKEN environment variable not found.");
|
||||
}
|
||||
const bot = new Bot(token);
|
||||
|
||||
|
||||
async function initializeAgent(userId: string) {
|
||||
try {
|
||||
const llm = new ChatOpenAI({
|
||||
@@ -24,7 +25,7 @@ async function initializeAgent(userId: string) {
|
||||
const solanaKit = new SolanaAgentKit(
|
||||
process.env.SOLANA_PRIVATE_KEY!,
|
||||
process.env.RPC_URL,
|
||||
process.env.OPENAI_API_KEY!
|
||||
process.env.OPENAI_API_KEY!,
|
||||
);
|
||||
|
||||
const tools = createSolanaTools(solanaKit);
|
||||
@@ -51,24 +52,40 @@ async function initializeAgent(userId: string) {
|
||||
}
|
||||
}
|
||||
// Telegram bot handler
|
||||
bot.on('message:text', async (ctx:any) => {
|
||||
bot.on("message:text", async (ctx: any) => {
|
||||
const userId = ctx.from?.id.toString();
|
||||
if (!userId) return;
|
||||
const {agent, config} = await initializeAgent(userId);
|
||||
const stream = await agent.stream({ messages: [new HumanMessage(ctx.message.text)] }, config);
|
||||
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 20000));
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const { agent, config } = await initializeAgent(userId);
|
||||
const stream = await agent.stream(
|
||||
{ messages: [new HumanMessage(ctx.message.text)] },
|
||||
config,
|
||||
);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), 20000),
|
||||
);
|
||||
try {
|
||||
for await (const chunk of await Promise.race([stream, timeoutPromise]) as AsyncIterable<{ agent?: any; tools?: any }>) {
|
||||
for await (const chunk of (await Promise.race([
|
||||
stream,
|
||||
timeoutPromise,
|
||||
])) as AsyncIterable<{ agent?: any; tools?: any }>) {
|
||||
if ("agent" in chunk) {
|
||||
if (chunk.agent.messages[0].content) await ctx.reply(String(chunk.agent.messages[0].content));
|
||||
}
|
||||
if (chunk.agent.messages[0].content) {
|
||||
await ctx.reply(String(chunk.agent.messages[0].content));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Timeout') {
|
||||
await ctx.reply("I'm sorry, the operation took too long and timed out. Please try again.");
|
||||
if (error.message === "Timeout") {
|
||||
await ctx.reply(
|
||||
"I'm sorry, the operation took too long and timed out. Please try again.",
|
||||
);
|
||||
} else {
|
||||
console.error("Error processing stream:", error);
|
||||
await ctx.reply("I'm sorry, an error occurred while processing your request.");
|
||||
await ctx.reply(
|
||||
"I'm sorry, an error occurred while processing your request.",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -77,10 +94,10 @@ bot.on('message:text', async (ctx:any) => {
|
||||
export const POST = async (req: Request) => {
|
||||
// Mark the function as a background function for Vercel
|
||||
const headers = new Headers();
|
||||
headers.set('x-vercel-background', 'true');
|
||||
headers.set("x-vercel-background", "true");
|
||||
|
||||
const handler = webhookCallback(bot, 'std/http'); // Use the correct callback
|
||||
const handler = webhookCallback(bot, "std/http"); // Use the correct callback
|
||||
|
||||
// Handle the incoming webhook request
|
||||
return handler(req);
|
||||
};
|
||||
};
|
||||
|
||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@@ -442,8 +442,8 @@ packages:
|
||||
'@shikijs/engine-oniguruma@1.24.3':
|
||||
resolution: {integrity: sha512-iNnx950gs/5Nk+zrp1LuF+S+L7SKEhn8k9eXgFYPGhVshKppsYwRmW8tpmAMvILIMSDfrgqZ0w+3xWVQB//1Xw==}
|
||||
|
||||
'@shikijs/types@1.24.3':
|
||||
resolution: {integrity: sha512-FPMrJ69MNxhRtldRk69CghvaGlbbN3pKRuvko0zvbfa2dXp4pAngByToqS5OY5jvN8D7LKR4RJE8UvzlCOuViw==}
|
||||
'@shikijs/types@1.24.4':
|
||||
resolution: {integrity: sha512-0r0XU7Eaow0PuDxuWC1bVqmWCgm3XqizIaT7SM42K03vc69LGooT0U8ccSR44xP/hGlNx4FKhtYpV+BU6aaKAA==}
|
||||
|
||||
'@shikijs/vscode-textmate@9.3.1':
|
||||
resolution: {integrity: sha512-79QfK1393x9Ho60QFyLti+QfdJzRQCVLFb97kOIV7Eo9vQU/roINgk7m24uv0a7AUvN//RDH36FLjjK48v0s9g==}
|
||||
@@ -903,8 +903,8 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.4.0:
|
||||
resolution: {integrity: sha512-ZkD35Mx92acjB2yNJgziGqT9oKHEOxjTBTDRpOsRWtdecL/0jM3z5kM/CTzHWvHIen1GvkM85p6TuFfDGfc8/Q==}
|
||||
chalk@5.4.1:
|
||||
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
check-error@2.1.1:
|
||||
@@ -939,8 +939,8 @@ packages:
|
||||
create-require@1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
|
||||
cross-fetch@3.1.8:
|
||||
resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
|
||||
cross-fetch@3.2.0:
|
||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
@@ -1418,8 +1418,8 @@ packages:
|
||||
typeorm:
|
||||
optional: true
|
||||
|
||||
langsmith@0.2.13:
|
||||
resolution: {integrity: sha512-16EOM5nhU6GlMCKGm5sgBIAKOKzS2d30qcDZmF21kSLZJiUhUNTROwvYdqgZLrGfIIzmSMJHCKA7RFd5qf50uw==}
|
||||
langsmith@0.2.14:
|
||||
resolution: {integrity: sha512-ClAuAgSf3m9miMYotLEaZKQyKdaWlfjhebCuYco8bc6g72dU2VwTg31Bv4YINBq7EH2i1cMwbOiJxbOXPqjGig==}
|
||||
peerDependencies:
|
||||
openai: '*'
|
||||
peerDependenciesMeta:
|
||||
@@ -1999,7 +1999,7 @@ snapshots:
|
||||
bs58: 4.0.1
|
||||
buffer-layout: 1.2.2
|
||||
camelcase: 6.3.0
|
||||
cross-fetch: 3.1.8
|
||||
cross-fetch: 3.2.0
|
||||
crypto-hash: 1.3.0
|
||||
eventemitter3: 4.0.7
|
||||
pako: 2.1.0
|
||||
@@ -2090,7 +2090,7 @@ snapshots:
|
||||
camelcase: 6.3.0
|
||||
decamelize: 1.2.0
|
||||
js-tiktoken: 1.0.16
|
||||
langsmith: 0.2.13(openai@4.77.0(zod@3.24.1))
|
||||
langsmith: 0.2.14(openai@4.77.0(zod@3.24.1))
|
||||
mustache: 4.2.0
|
||||
p-queue: 6.6.2
|
||||
p-retry: 4.6.2
|
||||
@@ -2399,13 +2399,13 @@ snapshots:
|
||||
- utf-8-validate
|
||||
|
||||
'@scure/base@1.2.1': {}
|
||||
|
||||
|
||||
'@shikijs/engine-oniguruma@1.24.3':
|
||||
dependencies:
|
||||
'@shikijs/types': 1.24.3
|
||||
'@shikijs/types': 1.24.4
|
||||
'@shikijs/vscode-textmate': 9.3.1
|
||||
|
||||
'@shikijs/types@1.24.3':
|
||||
'@shikijs/types@1.24.4':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 9.3.1
|
||||
'@types/hast': 3.0.4
|
||||
@@ -2572,24 +2572,24 @@ snapshots:
|
||||
|
||||
'@solana/errors@2.0.0-preview.2':
|
||||
dependencies:
|
||||
chalk: 5.4.0
|
||||
chalk: 5.4.1
|
||||
commander: 12.1.0
|
||||
|
||||
'@solana/errors@2.0.0-preview.4(typescript@5.7.2)':
|
||||
dependencies:
|
||||
chalk: 5.4.0
|
||||
chalk: 5.4.1
|
||||
commander: 12.1.0
|
||||
typescript: 5.7.2
|
||||
|
||||
'@solana/errors@2.0.0-rc.1(typescript@4.9.5)':
|
||||
dependencies:
|
||||
chalk: 5.4.0
|
||||
chalk: 5.4.1
|
||||
commander: 12.1.0
|
||||
typescript: 4.9.5
|
||||
|
||||
'@solana/errors@2.0.0-rc.1(typescript@5.7.2)':
|
||||
dependencies:
|
||||
chalk: 5.4.0
|
||||
chalk: 5.4.1
|
||||
commander: 12.1.0
|
||||
typescript: 5.7.2
|
||||
|
||||
@@ -3136,7 +3136,7 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.4.0: {}
|
||||
chalk@5.4.1: {}
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
@@ -3160,7 +3160,7 @@ snapshots:
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
cross-fetch@3.1.8:
|
||||
cross-fetch@3.2.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
@@ -3612,7 +3612,7 @@ snapshots:
|
||||
js-tiktoken: 1.0.16
|
||||
js-yaml: 4.1.0
|
||||
jsonpointer: 5.0.1
|
||||
langsmith: 0.2.13(openai@4.77.0(zod@3.24.1))
|
||||
langsmith: 0.2.14(openai@4.77.0(zod@3.24.1))
|
||||
openapi-types: 12.1.3
|
||||
p-retry: 4.6.2
|
||||
uuid: 10.0.0
|
||||
@@ -3626,7 +3626,7 @@ snapshots:
|
||||
- encoding
|
||||
- openai
|
||||
|
||||
langsmith@0.2.13(openai@4.77.0(zod@3.24.1)):
|
||||
langsmith@0.2.14(openai@4.77.0(zod@3.24.1)):
|
||||
dependencies:
|
||||
'@types/uuid': 10.0.0
|
||||
commander: 10.0.1
|
||||
@@ -3755,6 +3755,7 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
|
||||
openai@4.77.0(zod@3.24.1):
|
||||
dependencies:
|
||||
'@types/node': 18.19.68
|
||||
|
||||
@@ -25,10 +25,13 @@ import {
|
||||
getTokenDataByTicker,
|
||||
stakeWithJup,
|
||||
sendCompressedAirdrop,
|
||||
createOrcaSingleSidedWhirlpool,
|
||||
orcaCreateSingleSidedLiquidityPool,
|
||||
orcaCreateCLMM,
|
||||
orcaOpenCenteredPositionWithLiquidity,
|
||||
orcaOpenSingleSidedPosition,
|
||||
FEE_TIERS,
|
||||
fetchPrice,
|
||||
pythFetchPrice,
|
||||
FEE_TIERS,
|
||||
getAllDomainsTLDs,
|
||||
getAllRegisteredAllDomains,
|
||||
getOwnedDomainsForTLD,
|
||||
@@ -36,9 +39,12 @@ import {
|
||||
getOwnedAllDomains,
|
||||
resolveAllDomains,
|
||||
create_gibwork_task,
|
||||
orcaClosePosition,
|
||||
orcaFetchPositions,
|
||||
rock_paper_scissor,
|
||||
create_TipLink,
|
||||
} from "../tools";
|
||||
|
||||
import {
|
||||
CollectionDeployment,
|
||||
CollectionOptions,
|
||||
@@ -209,15 +215,28 @@ export class SolanaAgentKit {
|
||||
);
|
||||
}
|
||||
|
||||
async createOrcaSingleSidedWhirlpool(
|
||||
depositTokenAmount: BN,
|
||||
async orcaClosePosition(positionMintAddress: PublicKey) {
|
||||
return orcaClosePosition(this, positionMintAddress);
|
||||
}
|
||||
|
||||
async orcaCreateCLMM(
|
||||
mintDeploy: PublicKey,
|
||||
mintPair: PublicKey,
|
||||
initialPrice: Decimal,
|
||||
feeTier: keyof typeof FEE_TIERS,
|
||||
) {
|
||||
return orcaCreateCLMM(this, mintDeploy, mintPair, initialPrice, feeTier);
|
||||
}
|
||||
|
||||
async orcaCreateSingleSidedLiquidityPool(
|
||||
depositTokenAmount: number,
|
||||
depositTokenMint: PublicKey,
|
||||
otherTokenMint: PublicKey,
|
||||
initialPrice: Decimal,
|
||||
maxPrice: Decimal,
|
||||
feeTier: keyof typeof FEE_TIERS,
|
||||
) {
|
||||
return createOrcaSingleSidedWhirlpool(
|
||||
return orcaCreateSingleSidedLiquidityPool(
|
||||
this,
|
||||
depositTokenAmount,
|
||||
depositTokenMint,
|
||||
@@ -228,6 +247,42 @@ export class SolanaAgentKit {
|
||||
);
|
||||
}
|
||||
|
||||
async orcaFetchPositions() {
|
||||
return orcaFetchPositions(this);
|
||||
}
|
||||
|
||||
async orcaOpenCenteredPositionWithLiquidity(
|
||||
whirlpoolAddress: PublicKey,
|
||||
priceOffsetBps: number,
|
||||
inputTokenMint: PublicKey,
|
||||
inputAmount: Decimal,
|
||||
) {
|
||||
return orcaOpenCenteredPositionWithLiquidity(
|
||||
this,
|
||||
whirlpoolAddress,
|
||||
priceOffsetBps,
|
||||
inputTokenMint,
|
||||
inputAmount,
|
||||
);
|
||||
}
|
||||
|
||||
async orcaOpenSingleSidedPosition(
|
||||
whirlpoolAddress: PublicKey,
|
||||
distanceFromCurrentPriceBps: number,
|
||||
widthBps: number,
|
||||
inputTokenMint: PublicKey,
|
||||
inputAmount: Decimal,
|
||||
): Promise<string> {
|
||||
return orcaOpenSingleSidedPosition(
|
||||
this,
|
||||
whirlpoolAddress,
|
||||
distanceFromCurrentPriceBps,
|
||||
widthBps,
|
||||
inputTokenMint,
|
||||
inputAmount,
|
||||
);
|
||||
}
|
||||
|
||||
async resolveAllDomains(domain: string): Promise<PublicKey | undefined> {
|
||||
return resolveAllDomains(this, domain);
|
||||
}
|
||||
|
||||
@@ -796,17 +796,13 @@ export class SolanaCompressedAirdropTool extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaCreateSingleSidedWhirlpoolTool extends Tool {
|
||||
name = "create_orca_single_sided_whirlpool";
|
||||
description = `Create a single-sided Whirlpool with liquidity.
|
||||
|
||||
Inputs (input is a JSON string):
|
||||
- depositTokenAmount: number, eg: 1000000000 (required, in units of deposit token including decimals)
|
||||
- depositTokenMint: string, eg: "DepositTokenMintAddress" (required, mint address of deposit token)
|
||||
- otherTokenMint: string, eg: "OtherTokenMintAddress" (required, mint address of other token)
|
||||
- initialPrice: number, eg: 0.001 (required, initial price of deposit token in terms of other token)
|
||||
- maxPrice: number, eg: 5.0 (required, maximum price at which liquidity is added)
|
||||
- feeTier: number, eg: 0.30 (required, fee tier for the pool)`;
|
||||
export class SolanaClosePostition extends Tool {
|
||||
name = "orca_close_position";
|
||||
description = `Closes an existing liquidity position in an Orca Whirlpool. This function fetches the position
|
||||
details using the provided mint address and closes the position with a 1% slippage.
|
||||
|
||||
Inputs (JSON string):
|
||||
- positionMintAddress: string, the address of the position mint that represents the liquidity position.`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
@@ -815,7 +811,102 @@ export class SolanaCreateSingleSidedWhirlpoolTool extends Tool {
|
||||
async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const inputFormat = JSON.parse(input);
|
||||
const depositTokenAmount = new BN(inputFormat.depositTokenAmount);
|
||||
const positionMintAddress = new PublicKey(
|
||||
inputFormat.positionMintAddress,
|
||||
);
|
||||
|
||||
const txId = await this.solanaKit.orcaClosePosition(positionMintAddress);
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: "Liquidity position closed successfully.",
|
||||
transaction: txId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaOrcaCreateCLMM extends Tool {
|
||||
name = "orca_create_clmm";
|
||||
description = `Create a Concentrated Liquidity Market Maker (CLMM) pool on Orca, the most efficient and capital-optimized CLMM on Solana. This function initializes a CLMM pool but does not add liquidity. You can add liquidity later using a centered position or a single-sided position.
|
||||
|
||||
Inputs (JSON string):
|
||||
- mintDeploy: string, the mint of the token you want to deploy (required).
|
||||
- mintPair: string, The mint of the token you want to pair the deployed mint with (required).
|
||||
- initialPrice: number, initial price of mintA in terms of mintB, e.g., 0.001 (required).
|
||||
- feeTier: number, fee tier in bps. Options: 1, 2, 4, 5, 16, 30, 65, 100, 200 (required).`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const inputFormat = JSON.parse(input);
|
||||
const mintA = new PublicKey(inputFormat.mintDeploy);
|
||||
const mintB = new PublicKey(inputFormat.mintPair);
|
||||
const initialPrice = new Decimal(inputFormat.initialPrice);
|
||||
const feeTier = inputFormat.feeTier;
|
||||
|
||||
if (!feeTier || !(feeTier in FEE_TIERS)) {
|
||||
throw new Error(
|
||||
`Invalid feeTier. Available options: ${Object.keys(FEE_TIERS).join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const txId = await this.solanaKit.orcaCreateCLMM(
|
||||
mintA,
|
||||
mintB,
|
||||
initialPrice,
|
||||
feeTier,
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message:
|
||||
"CLMM pool created successfully. Note: No liquidity was added.",
|
||||
transaction: txId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaOrcaCreateSingleSideLiquidityPool extends Tool {
|
||||
name = "orca_create_single_sided_liquidity_pool";
|
||||
description = `Create a single-sided liquidity pool on Orca, the most efficient and capital-optimized CLMM platform on Solana.
|
||||
|
||||
This function initializes a single-sided liquidity pool, ideal for community driven project, fair launches, and fundraising. Minimize price impact by setting a narrow price range.
|
||||
|
||||
Inputs (JSON string):
|
||||
- depositTokenAmount: number, in units of the deposit token including decimals, e.g., 1000000000 (required).
|
||||
- depositTokenMint: string, mint address of the deposit token, e.g., "DepositTokenMintAddress" (required).
|
||||
- otherTokenMint: string, mint address of the other token, e.g., "OtherTokenMintAddress" (required).
|
||||
- initialPrice: number, initial price of the deposit token in terms of the other token, e.g., 0.001 (required).
|
||||
- maxPrice: number, maximum price at which liquidity is added, e.g., 5.0 (required).
|
||||
- feeTier: number, fee tier for the pool in bps. Options: 1, 2, 4, 5, 16, 30, 65, 100, 200 (required).`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const inputFormat = JSON.parse(input);
|
||||
const depositTokenAmount = inputFormat.depositTokenAmount;
|
||||
const depositTokenMint = new PublicKey(inputFormat.depositTokenMint);
|
||||
const otherTokenMint = new PublicKey(inputFormat.otherTokenMint);
|
||||
const initialPrice = new Decimal(inputFormat.initialPrice);
|
||||
@@ -830,7 +921,7 @@ export class SolanaCreateSingleSidedWhirlpoolTool extends Tool {
|
||||
);
|
||||
}
|
||||
|
||||
const txId = await this.solanaKit.createOrcaSingleSidedWhirlpool(
|
||||
const txId = await this.solanaKit.orcaCreateSingleSidedLiquidityPool(
|
||||
depositTokenAmount,
|
||||
depositTokenMint,
|
||||
otherTokenMint,
|
||||
@@ -854,6 +945,137 @@ export class SolanaCreateSingleSidedWhirlpoolTool extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaOrcaFetchPositions extends Tool {
|
||||
name = "orca_fetch_positions";
|
||||
description = `Fetch all the liquidity positions in an Orca Whirlpool by owner. Returns an object with positiont mint addresses as keys and position status details as values.`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
async _call(): Promise<string> {
|
||||
try {
|
||||
const txId = await this.solanaKit.orcaFetchPositions();
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: "Liquidity positions fetched.",
|
||||
transaction: txId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaOrcaOpenCenteredPosition extends Tool {
|
||||
name = "orca_open_centered_position_with_liquidity";
|
||||
description = `Add liquidity to a CLMM by opening a centered position in an Orca Whirlpool, the most efficient liquidity pool on Solana.
|
||||
|
||||
Inputs (JSON string):
|
||||
- whirlpoolAddress: string, address of the Orca Whirlpool (required).
|
||||
- priceOffsetBps: number, bps offset (one side) from the current pool price, e.g., 500 for 5% (required).
|
||||
- inputTokenMint: string, mint address of the deposit token (required).
|
||||
- inputAmount: number, amount of the deposit token, e.g., 100.0 (required).`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const inputFormat = JSON.parse(input);
|
||||
const whirlpoolAddress = new PublicKey(inputFormat.whirlpoolAddress);
|
||||
const priceOffsetBps = parseInt(inputFormat.priceOffsetBps, 10);
|
||||
const inputTokenMint = new PublicKey(inputFormat.inputTokenMint);
|
||||
const inputAmount = new Decimal(inputFormat.inputAmount);
|
||||
|
||||
if (priceOffsetBps < 0) {
|
||||
throw new Error(
|
||||
"Invalid distanceFromCurrentPriceBps. It must be equal or greater than 0.",
|
||||
);
|
||||
}
|
||||
|
||||
const txId = await this.solanaKit.orcaOpenCenteredPositionWithLiquidity(
|
||||
whirlpoolAddress,
|
||||
priceOffsetBps,
|
||||
inputTokenMint,
|
||||
inputAmount,
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: "Centered liquidity position opened successfully.",
|
||||
transaction: txId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaOrcaOpenSingleSidedPosition extends Tool {
|
||||
name = "orca_open_single_sided_position";
|
||||
description = `Add liquidity to a CLMM by opening a single-sided position in an Orca Whirlpool, the most efficient liquidity pool on Solana.
|
||||
|
||||
Inputs (JSON string):
|
||||
- whirlpoolAddress: string, address of the Orca Whirlpool (required).
|
||||
- distanceFromCurrentPriceBps: number, distance in basis points from the current price for the position (required).
|
||||
- widthBps: number, width of the position in basis points (required).
|
||||
- inputTokenMint: string, mint address of the deposit token (required).
|
||||
- inputAmount: number, amount of the deposit token, e.g., 100.0 (required).`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const inputFormat = JSON.parse(input);
|
||||
const whirlpoolAddress = new PublicKey(inputFormat.whirlpoolAddress);
|
||||
const distanceFromCurrentPriceBps =
|
||||
inputFormat.distanceFromCurrentPriceBps;
|
||||
const widthBps = inputFormat.widthBps;
|
||||
const inputTokenMint = new PublicKey(inputFormat.inputTokenMint);
|
||||
const inputAmount = new Decimal(inputFormat.inputAmount);
|
||||
|
||||
if (distanceFromCurrentPriceBps < 0 || widthBps < 0) {
|
||||
throw new Error(
|
||||
"Invalid distanceFromCurrentPriceBps or width. It must be equal or greater than 0.",
|
||||
);
|
||||
}
|
||||
|
||||
const txId = await this.solanaKit.orcaOpenSingleSidedPosition(
|
||||
whirlpoolAddress,
|
||||
distanceFromCurrentPriceBps,
|
||||
widthBps,
|
||||
inputTokenMint,
|
||||
inputAmount,
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: "Single-sided liquidity position opened successfully.",
|
||||
transaction: txId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaRaydiumCreateAmmV4 extends Tool {
|
||||
name = "raydium_create_ammV4";
|
||||
description = `Raydium's Legacy AMM that requiers an OpenBook marketID
|
||||
@@ -1302,9 +1524,9 @@ export class SolanaRockPaperScissorsTool extends Tool {
|
||||
const result = await this.solanaKit.rockPaperScissors(
|
||||
Number(parsedInput['"amount"']),
|
||||
parsedInput['"choice"'].replace(/^"|"$/g, "") as
|
||||
| "rock"
|
||||
| "paper"
|
||||
| "scissors",
|
||||
| "rock"
|
||||
| "paper"
|
||||
| "scissors",
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
@@ -1394,7 +1616,12 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
|
||||
new SolanaRaydiumCreateClmm(solanaKit),
|
||||
new SolanaRaydiumCreateCpmm(solanaKit),
|
||||
new SolanaOpenbookCreateMarket(solanaKit),
|
||||
new SolanaCreateSingleSidedWhirlpoolTool(solanaKit),
|
||||
new SolanaClosePostition(solanaKit),
|
||||
new SolanaOrcaCreateCLMM(solanaKit),
|
||||
new SolanaOrcaCreateSingleSideLiquidityPool(solanaKit),
|
||||
new SolanaOrcaFetchPositions(solanaKit),
|
||||
new SolanaOrcaOpenCenteredPosition(solanaKit),
|
||||
new SolanaOrcaOpenSingleSidedPosition(solanaKit),
|
||||
new SolanaPythFetchPrice(solanaKit),
|
||||
new SolanaResolveDomainTool(solanaKit),
|
||||
new SolanaGetOwnedDomains(solanaKit),
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,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";
|
||||
|
||||
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 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}`);
|
||||
}
|
||||
}
|
||||
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,70 +1,71 @@
|
||||
import { SolanaAgentKit } from "../agent";
|
||||
import { Transaction, Keypair, TransactionInstruction } from "@solana/web3.js";
|
||||
import { Connection, ComputeBudgetProgram } from "@solana/web3.js";
|
||||
import {
|
||||
Keypair,
|
||||
Signer,
|
||||
TransactionInstruction,
|
||||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from "@solana/web3.js";
|
||||
import { ComputeBudgetProgram } from "@solana/web3.js";
|
||||
|
||||
const feeTiers = {
|
||||
min: 0.01,
|
||||
mid: 0.5,
|
||||
max: 0.95,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get priority fees for the current block
|
||||
* @param connection - Solana RPC connection
|
||||
* @returns Priority fees statistics and instructions for different fee levels
|
||||
*/
|
||||
export async function getPriorityFees(connection: Connection): Promise<{
|
||||
min: number;
|
||||
median: number;
|
||||
max: number;
|
||||
instructions?: {
|
||||
low: TransactionInstruction;
|
||||
medium: TransactionInstruction;
|
||||
high: TransactionInstruction;
|
||||
};
|
||||
export async function getComputeBudgetInstructions(
|
||||
agent: SolanaAgentKit,
|
||||
instructions: TransactionInstruction[],
|
||||
feeTier: keyof typeof feeTiers,
|
||||
): Promise<{
|
||||
blockhash: string;
|
||||
computeBudgetLimitInstruction: TransactionInstruction;
|
||||
computeBudgetPriorityFeeInstructions: TransactionInstruction;
|
||||
}> {
|
||||
try {
|
||||
// Get recent prioritization fees
|
||||
const priorityFees = await connection.getRecentPrioritizationFees();
|
||||
const blockhash = (await agent.connection.getLatestBlockhash()).blockhash;
|
||||
const messageV0 = new TransactionMessage({
|
||||
payerKey: agent.wallet_address,
|
||||
recentBlockhash: blockhash,
|
||||
instructions: instructions,
|
||||
}).compileToV0Message();
|
||||
const transaction = new VersionedTransaction(messageV0);
|
||||
const simulatedTx = agent.connection.simulateTransaction(transaction);
|
||||
const estimatedComputeUnits = (await simulatedTx).value.unitsConsumed;
|
||||
const safeComputeUnits = Math.ceil(
|
||||
estimatedComputeUnits
|
||||
? Math.max(estimatedComputeUnits + 100000, estimatedComputeUnits * 1.2)
|
||||
: 200000,
|
||||
);
|
||||
const computeBudgetLimitInstruction =
|
||||
ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: safeComputeUnits,
|
||||
});
|
||||
|
||||
if (!priorityFees.length) {
|
||||
return {
|
||||
min: 0,
|
||||
median: 0,
|
||||
max: 0,
|
||||
};
|
||||
}
|
||||
const priorityFee = await agent.connection
|
||||
.getRecentPrioritizationFees()
|
||||
.then(
|
||||
(fees) =>
|
||||
fees.sort((a, b) => a.prioritizationFee - b.prioritizationFee)[
|
||||
Math.floor(fees.length * feeTiers[feeTier])
|
||||
].prioritizationFee,
|
||||
);
|
||||
|
||||
// Sort fees by value
|
||||
const sortedFees = priorityFees
|
||||
.map((x) => x.prioritizationFee)
|
||||
.sort((a, b) => a - b);
|
||||
const computeBudgetPriorityFeeInstructions =
|
||||
ComputeBudgetProgram.setComputeUnitPrice({
|
||||
microLamports: priorityFee,
|
||||
});
|
||||
|
||||
// Calculate statistics
|
||||
const min = sortedFees[0] ?? 0;
|
||||
const max = sortedFees[sortedFees.length - 1] ?? 0;
|
||||
const mid = Math.floor(sortedFees.length / 2);
|
||||
const median =
|
||||
sortedFees.length % 2 === 0
|
||||
? ((sortedFees[mid - 1] ?? 0) + (sortedFees[mid] ?? 0)) / 2
|
||||
: (sortedFees[mid] ?? 0);
|
||||
|
||||
// Helper to create priority fee IX based on chosen strategy
|
||||
const createPriorityFeeIx = (fee: number) => {
|
||||
return ComputeBudgetProgram.setComputeUnitPrice({
|
||||
microLamports: fee,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
min,
|
||||
median,
|
||||
max,
|
||||
// Return instructions for different fee levels
|
||||
instructions: {
|
||||
low: createPriorityFeeIx(min),
|
||||
medium: createPriorityFeeIx(median),
|
||||
high: createPriorityFeeIx(max),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting priority fees:", error);
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
blockhash,
|
||||
computeBudgetLimitInstruction,
|
||||
computeBudgetPriorityFeeInstructions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,23 +76,53 @@ export async function getPriorityFees(connection: Connection): Promise<{
|
||||
*/
|
||||
export async function sendTx(
|
||||
agent: SolanaAgentKit,
|
||||
tx: Transaction,
|
||||
instructions: TransactionInstruction[],
|
||||
otherKeypairs?: Keypair[],
|
||||
) {
|
||||
tx.recentBlockhash = (await agent.connection.getLatestBlockhash()).blockhash;
|
||||
tx.feePayer = agent.wallet_address;
|
||||
const fees = await getPriorityFees(agent.connection);
|
||||
if (fees.instructions) {
|
||||
tx.add(fees.instructions.medium!);
|
||||
}
|
||||
const ixComputeBudget = await getComputeBudgetInstructions(
|
||||
agent,
|
||||
instructions,
|
||||
"mid",
|
||||
);
|
||||
const allInstructions = [
|
||||
ixComputeBudget.computeBudgetLimitInstruction,
|
||||
ixComputeBudget.computeBudgetPriorityFeeInstructions,
|
||||
...instructions,
|
||||
];
|
||||
const messageV0 = new TransactionMessage({
|
||||
payerKey: agent.wallet_address,
|
||||
recentBlockhash: ixComputeBudget.blockhash,
|
||||
instructions: allInstructions,
|
||||
}).compileToV0Message();
|
||||
const transaction = new VersionedTransaction(messageV0);
|
||||
transaction.sign([agent.wallet, ...(otherKeypairs ?? [])] as Signer[]);
|
||||
|
||||
tx.sign(agent.wallet, ...(otherKeypairs ?? []));
|
||||
const txid = await agent.connection.sendRawTransaction(tx.serialize());
|
||||
await agent.connection.confirmTransaction({
|
||||
signature: txid,
|
||||
blockhash: (await agent.connection.getLatestBlockhash()).blockhash,
|
||||
lastValidBlockHeight: (await agent.connection.getLatestBlockhash())
|
||||
.lastValidBlockHeight,
|
||||
});
|
||||
return txid;
|
||||
const timeoutMs = 90000;
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
const transactionStartTime = Date.now();
|
||||
|
||||
const signature = await agent.connection.sendTransaction(transaction, {
|
||||
maxRetries: 0,
|
||||
skipPreflight: false,
|
||||
});
|
||||
|
||||
const statuses = await agent.connection.getSignatureStatuses([signature]);
|
||||
if (statuses.value[0]) {
|
||||
if (!statuses.value[0].err) {
|
||||
return signature;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Transaction failed: ${statuses.value[0].err.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const elapsedTime = Date.now() - transactionStartTime;
|
||||
const remainingTime = Math.max(0, 1000 - elapsedTime);
|
||||
if (remainingTime > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
||||
}
|
||||
}
|
||||
throw new Error("Transaction timeout");
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ async function initializeAgent() {
|
||||
try {
|
||||
const llm = new ChatOpenAI({
|
||||
modelName: "gpt-4o-mini",
|
||||
temperature: 0.7,
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
let walletDataStr: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user