Merge branch 'main' into rudy5348/main

This commit is contained in:
aryan
2024-12-20 23:49:28 +05:30
43 changed files with 4059 additions and 1505 deletions

View File

@@ -1,5 +1,4 @@
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";;
import bs58 from "bs58";
import Decimal from "decimal.js";
import { DEFAULT_OPTIONS } from "../constants";
@@ -8,6 +7,8 @@ import {
deploy_token,
get_balance,
getTPS,
resolveSolDomain,
getPrimaryDomain,
launchPumpFunToken,
lendAsset,
mintCollectionNFT,
@@ -19,8 +20,15 @@ import {
request_faucet_funds,
trade,
transfer,
getTokenDataByAddress,
getTokenDataByTicker,
stakeWithJup,
sendCompressedAirdrop,
createOrcaSingleSidedWhirlpool,
FEE_TIERS
} from "../tools";
import { CollectionOptions, PumpFunTokenOptions } from "../types";
import { BN } from "@coral-xyz/anchor";
/**
* Main class for interacting with Solana blockchain
@@ -40,7 +48,7 @@ export class SolanaAgentKit {
constructor(
private_key: string,
rpc_url = "https://api.mainnet-beta.solana.com",
openai_api_key: string,
openai_api_key: string
) {
this.connection = new Connection(rpc_url);
this.wallet = Keypair.fromSecretKey(bs58.decode(private_key));
@@ -54,10 +62,13 @@ export class SolanaAgentKit {
}
async deployToken(
name: string,
uri: string,
symbol: string,
decimals: number = DEFAULT_OPTIONS.TOKEN_DECIMALS,
// initialSupply?: number
initialSupply?: number
) {
return deploy_token(this, decimals);
return deploy_token(this, name, uri, symbol, decimals, initialSupply);
}
async deployCollection(options: CollectionOptions) {
@@ -71,7 +82,7 @@ export class SolanaAgentKit {
async mintNFT(
collectionMint: PublicKey,
metadata: Parameters<typeof mintCollectionNFT>[2],
recipient?: PublicKey,
recipient?: PublicKey
) {
return mintCollectionNFT(this, collectionMint, metadata, recipient);
}
@@ -84,11 +95,19 @@ export class SolanaAgentKit {
return registerDomain(this, name, spaceKB);
}
async resolveSolDomain(domain: string) {
return resolveSolDomain(this, domain);
}
async getPrimaryDomain(account: PublicKey) {
return getPrimaryDomain(this, account);
}
async trade(
outputMint: PublicKey,
inputAmount: number,
inputMint?: PublicKey,
slippageBps: number = DEFAULT_OPTIONS.SLIPPAGE_BPS,
slippageBps: number = DEFAULT_OPTIONS.SLIPPAGE_BPS
) {
return trade(this, outputMint, inputAmount, inputMint, slippageBps);
}
@@ -101,12 +120,20 @@ export class SolanaAgentKit {
return getTPS(this);
}
async getTokenDataByAddress(mint: string) {
return getTokenDataByAddress(new PublicKey(mint));
}
async getTokenDataByTicker(ticker: string) {
return getTokenDataByTicker(ticker);
}
async launchPumpFunToken(
tokenName: string,
tokenTicker: string,
description: string,
imageUrl: string,
options?: PumpFunTokenOptions,
options?: PumpFunTokenOptions
) {
return launchPumpFunToken(
this,
@@ -114,10 +141,52 @@ export class SolanaAgentKit {
tokenTicker,
description,
imageUrl,
options,
options
);
}
async stake(amount: number) {
return stakeWithJup(this, amount);
}
async sendCompressedAirdrop(
mintAddress: string,
amount: number,
decimals: number,
recipients: string[],
priorityFeeInLamports: number,
shouldLog: boolean
): Promise<string[]> {
return await sendCompressedAirdrop(
this,
new PublicKey(mintAddress),
amount,
decimals,
recipients.map((recipient) => new PublicKey(recipient)),
priorityFeeInLamports,
shouldLog
);
}
async createOrcaSingleSidedWhirlpool(
depositTokenAmount: BN,
depositTokenMint: PublicKey,
otherTokenMint: PublicKey,
initialPrice: Decimal,
maxPrice: Decimal,
feeTier: keyof typeof FEE_TIERS,
) {
return createOrcaSingleSidedWhirlpool(
this,
depositTokenAmount,
depositTokenMint,
otherTokenMint,
initialPrice,
maxPrice,
feeTier
)
}
async raydiumCreateAmmV4(
marketId: PublicKey,

View File

@@ -1,9 +1,11 @@
import { PublicKey } from "@solana/web3.js";
import { BN } from "bn.js";
import Decimal from "decimal.js";
import { Tool } from "langchain/tools";
import { SolanaAgentKit } from "../index";
import { create_image } from "../tools/create_image";
import { fetchPrice } from "../tools/fetch_price";
import { BN } from "@coral-xyz/anchor";
import { FEE_TIERS } from "../tools";
import { toJSON } from "../utils/toJSON";
export class SolanaBalanceTool extends Tool {
@@ -56,7 +58,6 @@ export class SolanaTransferTool extends Tool {
protected async _call(input: string): Promise<string> {
try {
const parsedInput = JSON.parse(input);
console.log(parsedInput);
const recipient = new PublicKey(parsedInput.to);
const mintAddress = parsedInput.mint
@@ -89,38 +90,30 @@ export class SolanaTransferTool extends Tool {
export class SolanaDeployTokenTool extends Tool {
name = "solana_deploy_token";
description =
"Deploy a new SPL token. Input should be JSON string with: {decimals?: number, initialSupply?: number}";
description = `Deploy a new token on Solana blockchain.
Inputs (input is a JSON string):
name: string, eg "My Token" (required)
uri: string, eg "https://example.com/token.json" (required)
symbol: string, eg "MTK" (required)
decimals?: number, eg 9 (optional, defaults to 9)
initialSupply?: number, eg 1000000 (optional)`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
private validateInput(input: any): void {
if (
input.decimals !== undefined &&
(typeof input.decimals !== "number" ||
input.decimals < 0 ||
input.decimals > 9)
) {
throw new Error(
"decimals must be a number between 0 and 9 when provided"
);
}
if (
input.initialSupply !== undefined &&
(typeof input.initialSupply !== "number" || input.initialSupply <= 0)
) {
throw new Error("initialSupply must be a positive number when provided");
}
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = toJSON(input);
this.validateInput(parsedInput);
const parsedInput = JSON.parse(input);
const result = await this.solanaKit.deployToken(parsedInput.decimals);
const result = await this.solanaKit.deployToken(
parsedInput.name,
parsedInput.uri,
parsedInput.symbol,
parsedInput.decimals,
parsedInput.initialSupply
);
return JSON.stringify({
status: "success",
@@ -140,57 +133,20 @@ export class SolanaDeployTokenTool extends Tool {
export class SolanaDeployCollectionTool extends Tool {
name = "solana_deploy_collection";
description =
"Deploy a new NFT collection. Input should be JSON with: {name: string, uri: string, royaltyBasisPoints?: number, creators?: Array<{address: string, percentage: number}>}";
description = `Deploy a new NFT collection on Solana blockchain.
Inputs (input is a JSON string):
name: string, eg "My Collection" (required)
uri: string, eg "https://example.com/collection.json" (required)
royaltyBasisPoints?: number, eg 500 for 5% (optional)`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
private validateInput(input: any): void {
if (!input.name || typeof input.name !== "string") {
throw new Error("name is required and must be a string");
}
if (!input.uri || typeof input.uri !== "string") {
throw new Error("uri is required and must be a string");
}
if (
input.royaltyBasisPoints !== undefined &&
(typeof input.royaltyBasisPoints !== "number" ||
input.royaltyBasisPoints < 0 ||
input.royaltyBasisPoints > 10000)
) {
throw new Error(
"royaltyBasisPoints must be a number between 0 and 10000 when provided"
);
}
if (input.creators) {
if (!Array.isArray(input.creators)) {
throw new Error("creators must be an array when provided");
}
input.creators.forEach((creator: any, index: number) => {
if (!creator.address || typeof creator.address !== "string") {
throw new Error(
`creator[${index}].address is required and must be a string`
);
}
if (
typeof creator.percentage !== "number" ||
creator.percentage < 0 ||
creator.percentage > 100
) {
throw new Error(
`creator[${index}].percentage must be a number between 0 and 100`
);
}
});
}
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = toJSON(input);
this.validateInput(parsedInput);
const parsedInput = JSON.parse(input);
const result = await this.solanaKit.deployCollection(parsedInput);
@@ -212,50 +168,42 @@ export class SolanaDeployCollectionTool extends Tool {
export class SolanaMintNFTTool extends Tool {
name = "solana_mint_nft";
description =
"Mint a new NFT in a collection. Input should be JSON with: {collectionMint: string, metadata: {name: string, symbol: string, uri: string}, recipient?: string}";
description = `Mint a new NFT in a collection on Solana blockchain.
Inputs (input is a JSON string):
collectionMint: string, eg "J1S9H3QjnRtBbbuD4HjPV6RpRhwuk4zKbxsnCHuTgh9w" (required) - The address of the collection to mint into
name: string, eg "My NFT" (required)
uri: string, eg "https://example.com/nft.json" (required)
recipient?: string, eg "9aUn5swQzUTRanaaTwmszxiv89cvFwUCjEBv1vZCoT1u" (optional) - The wallet to receive the NFT, defaults to agent's wallet which is ${this.solanaKit.wallet_address.toString()}`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
private validateInput(input: any): void {
if (!input.collectionMint || typeof input.collectionMint !== "string") {
throw new Error("collectionMint is required and must be a string");
}
if (!input.metadata || typeof input.metadata !== "object") {
throw new Error("metadata is required and must be an object");
}
if (!input.metadata.name || typeof input.metadata.name !== "string") {
throw new Error("metadata.name is required and must be a string");
}
if (!input.metadata.symbol || typeof input.metadata.symbol !== "string") {
throw new Error("metadata.symbol is required and must be a string");
}
if (!input.metadata.uri || typeof input.metadata.uri !== "string") {
throw new Error("metadata.uri is required and must be a string");
}
if (input.recipient !== undefined && typeof input.recipient !== "string") {
throw new Error("recipient must be a string when provided");
}
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = toJSON(input);
this.validateInput(parsedInput);
const parsedInput = JSON.parse(input);
const result = await this.solanaKit.mintNFT(
new PublicKey(parsedInput.collectionMint),
parsedInput.metadata,
parsedInput.recipient ? new PublicKey(parsedInput.recipient) : undefined
{
name: parsedInput.name,
uri: parsedInput.uri,
},
parsedInput.recipient
? new PublicKey(parsedInput.recipient)
: this.solanaKit.wallet_address
);
return JSON.stringify({
status: "success",
message: "NFT minted successfully",
mintAddress: result.mint.toString(),
name: parsedInput.metadata.name,
metadata: {
name: parsedInput.name,
symbol: parsedInput.symbol,
uri: parsedInput.uri,
},
recipient: parsedInput.recipient || result.mint.toString(),
});
} catch (error: any) {
@@ -304,7 +252,6 @@ export class SolanaTradeTool extends Tool {
outputToken: parsedInput.outputMint,
});
} catch (error: any) {
console.log(error);
return JSON.stringify({
status: "error",
message: error.message,
@@ -393,6 +340,70 @@ export class SolanaRegisterDomainTool extends Tool {
}
}
export class SolanaResolveDomainTool extends Tool {
name = "solana_resolve_domain";
description = `Resolve a .sol domain to a Solana PublicKey.
Inputs:
domain: string, eg "pumpfun.sol" or "pumpfun"(required)
`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const domain = input.trim();
const publicKey = await this.solanaKit.resolveSolDomain(domain);
return JSON.stringify({
status: "success",
message: "Domain resolved successfully",
publicKey: publicKey.toBase58(),
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaGetDomainTool extends Tool {
name = "solana_get_domain";
description = `Retrieve the .sol domain associated for a given account address.
Inputs:
account: string, eg "4Be9CvxqHW6BYiRAxW9Q3xu1ycTMWaL5z8NX4HR3ha7t" (required)
`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const account = new PublicKey(input.trim());
const domain = await this.solanaKit.getPrimaryDomain(account);
return JSON.stringify({
status: "success",
message: "Primary domain retrieved successfully",
domain,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaGetWalletAddressTool extends Tool {
name = "solana_get_wallet_address";
description = `Get the wallet address of the agent`;
@@ -425,7 +436,6 @@ export class SolanaPumpfunTokenLaunchTool extends Tool {
}
private validateInput(input: any): void {
console.log(input);
if (!input.tokenName || typeof input.tokenName !== "string") {
throw new Error("tokenName is required and must be a string");
}
@@ -570,6 +580,230 @@ export class SolanaTPSCalculatorTool extends Tool {
}
}
export class SolanaStakeTool extends Tool {
name = "solana_stake";
description = `This tool can be used to stake your SOL (Solana), also called as SOL staking or liquid staking.
Inputs ( input is a JSON string ):
amount: number, eg 1 or 0.01 (required)`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = JSON.parse(input) || Number(input);
const tx = await this.solanaKit.stake(parsedInput.amount);
return JSON.stringify({
status: "success",
message: "Staked successfully",
transaction: tx,
amount: parsedInput.amount,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
/**
* Tool to fetch the price of a token in USDC
*/
export class SolanaFetchPriceTool extends Tool {
name = "solana_fetch_price";
description = `Fetch the price of a given token in USDC.
Inputs:
- tokenId: string, the mint address of the token, e.g., "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
async _call(input: string): Promise<string> {
try {
const price = await fetchPrice(this.solanaKit, input.trim());
return JSON.stringify({
status: "success",
tokenId: input.trim(),
priceInUSDC: price,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaTokenDataTool extends Tool {
name = "solana_token_data";
description = `Get the token data for a given token mint address
Inputs: mintAddress is required.
mintAddress: string, eg "So11111111111111111111111111111111111111112" (required)`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = input.trim();
const tokenData = await this.solanaKit.getTokenDataByAddress(parsedInput);
return JSON.stringify({
status: "success",
tokenData: tokenData,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaTokenDataByTickerTool extends Tool {
name = "solana_token_data_by_ticker";
description = `Get the token data for a given token ticker
Inputs: ticker is required.
ticker: string, eg "USDC" (required)`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const ticker = input.trim();
const tokenData = await this.solanaKit.getTokenDataByTicker(ticker);
return JSON.stringify({
status: "success",
tokenData: tokenData,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaCompressedAirdropTool extends Tool {
name = "solana_compressed_airdrop";
description = `Airdrop SPL tokens with ZK Compression (also called as airdropping tokens)
Inputs (input is a JSON string):
mintAddress: string, the mint address of the token, e.g., "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" (required)
amount: number, the amount of tokens to airdrop per recipient, e.g., 42 (required)
decimals: number, the decimals of the token, e.g., 6 (required)
recipients: string[], the recipient addresses, e.g., ["1nc1nerator11111111111111111111111111111111"] (required)
priorityFeeInLamports: number, the priority fee in lamports. Default is 30_000. (optional)
shouldLog: boolean, whether to log progress to stdout. Default is false. (optional)`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = JSON.parse(input);
const txs = await this.solanaKit.sendCompressedAirdrop(
parsedInput.mintAddress,
parsedInput.amount,
parsedInput.decimals,
parsedInput.recipients,
parsedInput.priorityFeeInLamports || 30_000,
parsedInput.shouldLog || false
);
return JSON.stringify({
status: "success",
message: `Airdropped ${parsedInput.amount} tokens to ${parsedInput.recipients.length} recipients.`,
transactionHashes: txs,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
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)`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const depositTokenAmount = new BN(inputFormat.depositTokenAmount);
const depositTokenMint = new PublicKey(inputFormat.depositTokenMint);
const otherTokenMint = new PublicKey(inputFormat.otherTokenMint);
const initialPrice = new Decimal(inputFormat.initialPrice);
const maxPrice = new Decimal(inputFormat.maxPrice);
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.createOrcaSingleSidedWhirlpool(
depositTokenAmount,
depositTokenMint,
otherTokenMint,
initialPrice,
maxPrice,
feeTier,
);
return JSON.stringify({
status: "success",
message: "Single-sided Whirlpool created 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
@@ -761,9 +995,17 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
new SolanaCreateImageTool(solanaKit),
new SolanaLendAssetTool(solanaKit),
new SolanaTPSCalculatorTool(solanaKit),
new SolanaStakeTool(solanaKit),
new SolanaFetchPriceTool(solanaKit),
new SolanaResolveDomainTool(solanaKit),
new SolanaGetDomainTool(solanaKit),
new SolanaTokenDataTool(solanaKit),
new SolanaTokenDataByTickerTool(solanaKit),
new SolanaCompressedAirdropTool(solanaKit),
new SolanaRaydiumCreateAmmV4(solanaKit),
new SolanaRaydiumCreateClmm(solanaKit),
new SolanaRaydiumCreateCpmm(solanaKit),
new SolanaOpenbookCreateMarket(solanaKit),
new SolanaCreateSingleSidedWhirlpoolTool(solanaKit),
];
}

View File

@@ -0,0 +1,385 @@
import { Keypair, PublicKey } 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,
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";
/**
* 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.30: 64,
0.65: 96,
1.00: 128,
2.00: 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 txId = await txBuilder.buildAndExecute({
maxSupportedTransactionVersion: "legacy"
});
return txId;
}

View File

@@ -1,9 +1,9 @@
import { SolanaAgentKit } from "../index";
import { createUmi, generateSigner, publicKey } from "@metaplex-foundation/umi";
import { createCollection, ruleSet } from "@metaplex-foundation/mpl-core";
import { mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata";
import { generateSigner, keypairIdentity, publicKey } from "@metaplex-foundation/umi";
import { createCollection, mplCore, ruleSet } from "@metaplex-foundation/mpl-core";
import { CollectionOptions, CollectionDeployment } from "../types";
import { toWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters";
import { fromWeb3JsKeypair, toWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
/**
* Deploy a new NFT collection
@@ -17,7 +17,8 @@ export async function deploy_collection(
): Promise<CollectionDeployment> {
try {
// Initialize Umi
const umi = createUmi().use(mplTokenMetadata());
const umi = createUmi(agent.connection.rpcEndpoint).use(mplCore());
umi.use(keypairIdentity(fromWeb3JsKeypair(agent.wallet)));
// Generate collection signer
const collectionSigner = generateSigner(umi);
@@ -27,11 +28,11 @@ export async function deploy_collection(
address: publicKey(creator.address),
percentage: creator.percentage,
})) || [
{
address: publicKey(agent.wallet_address.toString()),
percentage: 100,
},
];
{
address: publicKey(agent.wallet_address.toString()),
percentage: 100,
},
];
// Create collection
const tx = await createCollection(umi, {

View File

@@ -1,68 +1,66 @@
import { SolanaAgentKit } from "../index";
import {
createInitializeMint2Instruction,
MINT_SIZE,
getMinimumBalanceForRentExemptAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { Keypair, SystemProgram, Transaction } from "@solana/web3.js";
import { sendTx } from "../utils/send_tx";
import { PublicKey } from "@solana/web3.js";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { generateSigner, keypairIdentity } from "@metaplex-foundation/umi";
import { createFungible, mintV1, TokenStandard } from "@metaplex-foundation/mpl-token-metadata";
import { fromWeb3JsKeypair, fromWeb3JsPublicKey, toWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters";
/**
* Deploy a new SPL token
* @param agent SolanaAgentKit instance
* @param name Name of the token
* @param uri URI for the token metadata
* @param symbol Symbol of the token
* @param decimals Number of decimals for the token (default: 9)
* @param initialSupply Initial supply to mint (optional)
* @returns Object containing token mint address and initial account (if supply was minted)
*/
export async function deploy_token(
agent: SolanaAgentKit,
decimals: number = 9
// initialSupply?: number
) {
name: string,
uri: string,
symbol: string,
decimals: number = 9,
initialSupply?: number
): Promise<{ mint: PublicKey }> {
try {
// Create UMI instance from agent
const umi = createUmi(agent.connection.rpcEndpoint)
umi.use(keypairIdentity(fromWeb3JsKeypair(agent.wallet)));
// Create new token mint
const lamports = await getMinimumBalanceForRentExemptAccount(
agent.connection
);
const mint = generateSigner(umi);
const mint = Keypair.generate();
console.log("Mint address: ", mint.publicKey.toString());
console.log("Agent address: ", agent.wallet_address.toString());
let account_create_ix = SystemProgram.createAccount({
fromPubkey: agent.wallet_address,
newAccountPubkey: mint.publicKey,
lamports,
space: MINT_SIZE,
programId: TOKEN_PROGRAM_ID,
let builder = createFungible(umi, {
name,
uri,
symbol,
sellerFeeBasisPoints: {
basisPoints: 0n,
identifier: "%",
decimals: 2,
},
decimals,
mint,
});
let create_mint_ix = createInitializeMint2Instruction(
mint.publicKey,
decimals,
agent.wallet_address,
agent.wallet_address,
TOKEN_PROGRAM_ID
);
if (initialSupply) {
builder = builder.add(
mintV1(umi, {
mint: mint.publicKey,
tokenStandard: TokenStandard.Fungible,
tokenOwner: fromWeb3JsPublicKey(agent.wallet_address),
amount: initialSupply,
})
);
}
let tx = new Transaction().add(account_create_ix, create_mint_ix);
let hash = await sendTx(agent, tx, [mint]);
console.log("Transaction hash: ", hash);
console.log(
"Token deployed successfully. Mint address: ",
mint.publicKey.toString()
);
builder.sendAndConfirm(umi, { confirm: { commitment: 'finalized' } });
return {
mint: mint.publicKey,
mint: toWeb3JsPublicKey(mint.publicKey),
};
} catch (error: any) {
console.log(error);
throw new Error(`Token deployment failed: ${error.message}`);
}
}

35
src/tools/fetch_price.ts Normal file
View File

@@ -0,0 +1,35 @@
import { SolanaAgentKit } from "../index";
import { Tool } from "langchain/tools";
/**
* Fetch the price of a given token in USDC using Jupiter API
* @param agent SolanaAgentKit instance
* @param tokenId The token mint address
* @returns The price of the token in USDC
*/
export async function fetchPrice(
agent: SolanaAgentKit,
tokenId: string
): Promise<string> {
try {
const response = await fetch(
`https://api.jup.ag/price/v2?ids=${tokenId}`
);
if (!response.ok) {
throw new Error(`Failed to fetch price: ${response.statusText}`);
}
const data = await response.json();
const price = data.data[tokenId]?.price;
if (!price) {
throw new Error("Price data not available for the given token.");
}
return price;
} catch (error: any) {
throw new Error(`Price fetch failed: ${error.message}`);
}
}

View File

@@ -0,0 +1,37 @@
import { getPrimaryDomain as _getPrimaryDomain } from "@bonfida/spl-name-service";
import { PublicKey } from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
/**
* Retrieves the primary .sol domain associated with a given Solana public key.
*
* This function queries the Bonfida SPL Name Service to get the primary .sol domain for
* a specified Solana public key. If the primary domain is stale or an error occurs during
* the resolution, it throws an error.
*
* @param agent SolanaAgentKit instance
* @param account The Solana public key for which to retrieve the primary domain
* @returns A promise that resolves to the primary .sol domain as a string
* @throws Error if the domain is stale or if the domain resolution fails
*/
export async function getPrimaryDomain(
agent: SolanaAgentKit,
account: PublicKey
): Promise<string> {
try {
const { reverse, stale } = await _getPrimaryDomain(
agent.connection,
account
);
if (stale) {
throw new Error(
`Primary domain is stale for account: ${account.toBase58()}`
);
}
return reverse;
} catch (error) {
throw new Error(
`Failed to get primary domain for account: ${account.toBase58()}`
);
}
}

View File

@@ -0,0 +1,68 @@
import { PublicKey } from "@solana/web3.js";
import { JupiterTokenData } from "../types";
export async function getTokenDataByAddress(
mint: PublicKey,
): Promise<JupiterTokenData | undefined> {
try {
if (!mint) {
throw new Error("Mint address is required");
}
const response = await fetch("https://tokens.jup.ag/tokens?tags=verified", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = (await response.json()) as JupiterTokenData[];
const token = data.find((token: JupiterTokenData) => {
return token.address === mint.toBase58();
});
return token;
} catch (error: any) {
throw new Error(`Error fetching token data: ${error.message}`);
}
}
export async function getTokenAddressFromTicker(
ticker: string
): Promise<string | null> {
try {
const response = await fetch(
`https://api.dexscreener.com/latest/dex/search?q=${ticker}`
);
const data = await response.json();
if (!data.pairs || data.pairs.length === 0) {
return null;
}
// Filter for Solana pairs only and sort by FDV
let solanaPairs = data.pairs
.filter((pair: any) => pair.chainId === "solana")
.sort((a: any, b: any) => (b.fdv || 0) - (a.fdv || 0));
solanaPairs = solanaPairs.filter(
(pair: any) =>
pair.baseToken.symbol.toLowerCase() === ticker.toLowerCase()
);
// Return the address of the highest FDV Solana pair
return solanaPairs[0].baseToken.address;
} catch (error) {
console.error("Error fetching token address from DexScreener:", error);
return null;
}
}
export async function getTokenDataByTicker(
ticker: string
): Promise<JupiterTokenData | undefined> {
const address = await getTokenAddressFromTicker(ticker);
if (!address) {
throw new Error(`Token address not found for ticker: ${ticker}`);
}
return getTokenDataByAddress(new PublicKey(address));
}

View File

@@ -6,10 +6,17 @@ export * from "./mint_nft";
export * from "./transfer";
export * from "./trade";
export * from "./register_domain";
export * from "./resolve_sol_domain";
export * from "./get_primary_domain";
export * from "./launch_pumpfun_token";
export * from "./lend";
export * from "./get_tps";
export * from "./raydium_create_ammV4";
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 "./raydium_create_ammV4";
export * from "./raydium_create_clmm";
export * from "./raydium_create_cpmm";
export * from "./openbook_create_market";

View File

@@ -38,7 +38,6 @@ async function uploadMetadata(
finalFormData.append('file', files.file);
}
console.log("Final form data:", finalFormData);
const metadataResponse = await fetch("https://pump.fun/api/ipfs", {
method: "POST",
@@ -46,7 +45,6 @@ async function uploadMetadata(
});
if (!metadataResponse.ok) {
console.log("Metadata response:", await metadataResponse.json());
throw new Error(`Metadata upload failed: ${metadataResponse.statusText}`);
}
@@ -152,30 +150,14 @@ export async function launchPumpFunToken(
options?: PumpFunTokenOptions
) {
try {
// TBD : Remove clgs after approval
console.log("Starting token launch process...");
// Generate mint keypair
const mintKeypair = Keypair.generate();
console.log("Mint public key:", mintKeypair.publicKey.toBase58());
// Upload metadata
console.log("Uploading metadata to IPFS...");
const metadataResponse = await uploadMetadata(tokenName, tokenTicker, description, imageUrl, options);
console.log("Metadata response:", metadataResponse);
// Create token transaction
console.log("Creating token transaction...");
const response = await createTokenTransaction(agent, mintKeypair, metadataResponse, options);
const transactionData = await response.arrayBuffer();
const tx = VersionedTransaction.deserialize(new Uint8Array(transactionData));
// Send transaction with proper blockhash handling
console.log("Sending transaction...");
const signature = await signAndSendTransaction(agent, tx, mintKeypair);
console.log("Token launch successful!");
return {
signature,
mint: mintKeypair.publicKey.toBase58(),

View File

@@ -1,9 +1,9 @@
import { SolanaAgentKit } from "../index";
import { generateSigner } from '@metaplex-foundation/umi';
import { create } from '@metaplex-foundation/mpl-core';
import { generateSigner, keypairIdentity } from '@metaplex-foundation/umi';
import { create, mplCore } from '@metaplex-foundation/mpl-core';
import { fetchCollection } from '@metaplex-foundation/mpl-core';
import { PublicKey } from "@solana/web3.js";
import { fromWeb3JsPublicKey, toWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters";
import { fromWeb3JsKeypair, fromWeb3JsPublicKey, toWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters";
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { MintCollectionNFTResponse } from '../types';
@@ -20,7 +20,6 @@ export async function mintCollectionNFT(
collectionMint: PublicKey,
metadata: {
name: string;
symbol: string;
uri: string;
sellerFeeBasisPoints?: number;
creators?: Array<{
@@ -32,11 +31,12 @@ export async function mintCollectionNFT(
): Promise<MintCollectionNFTResponse> {
try {
// Create UMI instance from agent
const umi = createUmi(agent.connection)
const umi = createUmi(agent.connection.rpcEndpoint).use(mplCore());
umi.use(keypairIdentity(fromWeb3JsKeypair(agent.wallet)));
// Convert collection mint to UMI format
const umiCollectionMint = fromWeb3JsPublicKey(collectionMint);
// Fetch the existing collection
const collection = await fetchCollection(umi, umiCollectionMint);
@@ -48,8 +48,8 @@ export async function mintCollectionNFT(
asset: assetSigner,
collection: collection,
name: metadata.name,
uri: metadata.uri,
owner: fromWeb3JsPublicKey(recipient!)
uri: metadata.uri,
owner: fromWeb3JsPublicKey(recipient ?? agent.wallet.publicKey)
}).sendAndConfirm(umi);
return {

View File

@@ -0,0 +1,30 @@
import { resolve } from "@bonfida/spl-name-service";
import { PublicKey } from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
/**
* Resolves a .sol domain to a Solana PublicKey.
*
* This function uses the Bonfida SPL Name Service to resolve a given .sol domain
* to the corresponding Solana PublicKey. The domain can be provided with or without
* the .sol suffix.
*
* @param agent SolanaAgentKit instance
* @param domain The .sol domain to resolve. This can be provided with or without the .sol TLD suffix
* @returns A promise that resolves to the corresponding Solana PublicKey
* @throws Error if the domain resolution fails
*/
export async function resolveSolDomain(
agent: SolanaAgentKit,
domain: string
): Promise<PublicKey> {
if (!domain || typeof domain !== "string") {
throw new Error("Invalid domain. Expected a non-empty string.");
}
try {
return await resolve(agent.connection, domain);
} catch (error) {
throw new Error(`Failed to resolve domain: ${domain}`);
}
}

View File

@@ -0,0 +1,306 @@
import {
AddressLookupTableAccount,
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../agent/index.js";
import {
buildAndSignTx,
calculateComputeUnitPrice,
createRpc,
Rpc,
sendAndConfirmTx,
sleep,
} from "@lightprotocol/stateless.js";
import {
CompressedTokenProgram,
createTokenPool,
} from "@lightprotocol/compressed-token";
import { Account, getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
// arbitrary
const MAX_AIRDROP_RECIPIENTS = 1000;
const MAX_CONCURRENT_TXS = 30;
/**
* Estimate the cost of an airdrop in lamports.
* @param numberOfRecipients Number of recipients
* @param priorityFeeInLamports Priority fee in lamports
* @returns Estimated cost in lamports
*/
export const getAirdropCostEstimate = (
numberOfRecipients: number,
priorityFeeInLamports: number
) => {
const baseFee = 5000;
const perRecipientCompressedStateFee = 300;
const txsNeeded = Math.ceil(numberOfRecipients / 15);
const totalPriorityFees = txsNeeded * (baseFee + priorityFeeInLamports);
return (
perRecipientCompressedStateFee * numberOfRecipients + totalPriorityFees
);
};
/**
* Send airdrop with ZK Compressed Tokens.
* @param agent Agent
* @param mintAddress SPL Mint address
* @param amount Amount to send per recipient
* @param decimals Decimals of the token
* @param recipients Recipient wallet addresses (no ATAs)
* @param priorityFeeInLamports Priority fee in lamports
* @param shouldLog Whether to log progress to stdout. Defaults to false.
*/
export async function sendCompressedAirdrop(
agent: SolanaAgentKit,
mintAddress: PublicKey,
amount: number,
decimals: number,
recipients: PublicKey[],
priorityFeeInLamports: number,
shouldLog: boolean = false
): Promise<string[]> {
if (recipients.length > MAX_AIRDROP_RECIPIENTS) {
throw new Error(
`Max airdrop can be ${MAX_AIRDROP_RECIPIENTS} recipients at a time. For more scale, use open source ZK Compression airdrop tools such as https://github.com/helius-labs/airship.`
);
}
const url = agent.connection.rpcEndpoint;
if (url.includes("devnet")) {
throw new Error("Devnet is not supported for airdrop. Please use mainnet.");
}
if (!url.includes("helius")) {
console.warn(
"Warning: Must use RPC with ZK Compression support. Double check with your RPC provider if in doubt."
);
}
let sourceTokenAccount: Account;
try {
sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
agent.connection,
agent.wallet,
mintAddress,
agent.wallet.publicKey
);
} catch (error) {
throw new Error(
"Source token account not found and failed to create it. Please add funds to your wallet and try again."
);
}
try {
await createTokenPool(
agent.connection as unknown as Rpc,
agent.wallet,
mintAddress
);
} catch (error: any) {
if (error.message.includes("already in use")) {
// skip
} else {
throw error;
}
}
return await processAll(
agent,
amount * 10 ** decimals,
mintAddress,
recipients,
priorityFeeInLamports,
shouldLog
);
}
async function processAll(
agent: SolanaAgentKit,
amount: number,
mint: PublicKey,
recipients: PublicKey[],
priorityFeeInLamports: number,
shouldLog: boolean
): Promise<string[]> {
const mintAddress = mint;
const payer = agent.wallet;
const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
agent.connection,
agent.wallet,
mintAddress,
agent.wallet.publicKey
);
const maxRecipientsPerInstruction = 5;
const maxIxs = 3; // empirically determined (as of 12/15/2024)
const lookupTableAddress = new PublicKey(
"9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"
);
const lookupTableAccount = (
await agent.connection.getAddressLookupTable(lookupTableAddress)
).value!;
const batches: PublicKey[][] = [];
for (
let i = 0;
i < recipients.length;
i += maxRecipientsPerInstruction * maxIxs
) {
batches.push(recipients.slice(i, i + maxRecipientsPerInstruction * maxIxs));
}
const instructionSets = await Promise.all(
batches.map(async (recipientBatch) => {
const instructions: TransactionInstruction[] = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: calculateComputeUnitPrice(
priorityFeeInLamports,
500_000
),
}),
];
const compressIxPromises = [];
for (
let i = 0;
i < recipientBatch.length;
i += maxRecipientsPerInstruction
) {
const batch = recipientBatch.slice(i, i + maxRecipientsPerInstruction);
compressIxPromises.push(
CompressedTokenProgram.compress({
payer: payer.publicKey,
owner: payer.publicKey,
source: sourceTokenAccount.address,
toAddress: batch,
amount: batch.map(() => amount),
mint: mintAddress,
})
);
}
const compressIxs = await Promise.all(compressIxPromises);
return [...instructions, ...compressIxs];
})
);
const url = agent.connection.rpcEndpoint;
const rpc = createRpc(url, url, url);
const results = [];
let confirmedCount = 0;
const totalBatches = instructionSets.length;
const renderProgressBar = (current: number, total: number) => {
const percentage = Math.floor((current / total) * 100);
const filled = Math.floor((percentage / 100) * 20);
const empty = 20 - filled;
const bar = "█".repeat(filled) + "░".repeat(empty);
return `Airdropped to ${Math.min(current * 15, recipients.length)}/${
recipients.length
} recipients [${bar}] ${percentage}%`;
};
const log = (message: string) => {
if (shouldLog && typeof process !== "undefined" && process.stdout) {
process.stdout.write(message);
}
};
for (let i = 0; i < instructionSets.length; i += MAX_CONCURRENT_TXS) {
const batchPromises = instructionSets
.slice(i, i + MAX_CONCURRENT_TXS)
.map((instructions, idx) =>
sendTransactionWithRetry(
rpc,
instructions,
payer,
lookupTableAccount,
i + idx
).then((signature) => {
confirmedCount++;
log("\r" + renderProgressBar(confirmedCount, totalBatches));
return signature;
})
);
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
}
log("\n");
const failures = results
.filter((r) => r.status === "rejected")
.map((r, idx) => ({
index: idx,
error: (r as PromiseRejectedResult).reason,
}));
if (failures.length > 0) {
throw new Error(
`Failed to process ${failures.length} batches: ${failures
.map((f) => f.error)
.join(", ")}`
);
}
return results.map((r) => (r as PromiseFulfilledResult<string>).value);
}
async function sendTransactionWithRetry(
connection: Rpc,
instructions: TransactionInstruction[],
payer: Keypair,
lookupTableAccount: AddressLookupTableAccount,
batchIndex: number
): Promise<string> {
const MAX_RETRIES = 3;
const INITIAL_BACKOFF = 500; // ms
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
const { blockhash } = await connection.getLatestBlockhash();
const tx = buildAndSignTx(
instructions,
payer,
blockhash,
[],
[lookupTableAccount]
);
const signature = await sendAndConfirmTx(connection, tx);
return signature;
} catch (error: any) {
const isRetryable =
error.message?.includes("blockhash not found") ||
error.message?.includes("timeout") ||
error.message?.includes("rate limit") ||
error.message?.includes("too many requests");
if (!isRetryable || attempt === MAX_RETRIES - 1) {
throw new Error(
`Batch ${batchIndex} failed after ${attempt + 1} attempts: ${
error.message
}`
);
}
const backoff =
INITIAL_BACKOFF * Math.pow(2, attempt) * (0.5 + Math.random());
await sleep(backoff);
}
}
throw new Error("Unreachable");
}

View File

@@ -0,0 +1,56 @@
import { VersionedTransaction } from "@solana/web3.js";
import { SolanaAgentKit } from "../agent";
/**
* Stake SOL with Jup validator
* @param agent SolanaAgentKit instance
* @param amount Amount of SOL to stake
* @returns Transaction signature
*/
export async function stakeWithJup(
agent: SolanaAgentKit,
amount: number,
): Promise<string> {
try {
const res = await fetch(
`https://worker.jup.ag/blinks/swap/So11111111111111111111111111111111111111112/jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v/${amount}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
account: agent.wallet.publicKey.toBase58(),
}),
},
);
const data = await res.json();
const txn = VersionedTransaction.deserialize(
Buffer.from(data.transaction, "base64"),
);
const { blockhash } = await agent.connection.getLatestBlockhash();
txn.message.recentBlockhash = blockhash;
// Sign and send transaction
txn.sign([agent.wallet]);
const signature = await agent.connection.sendTransaction(txn, {
preflightCommitment: "confirmed",
maxRetries: 3,
});
const latestBlockhash = await agent.connection.getLatestBlockhash();
await agent.connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
});
return signature;
} catch (error: any) {
console.error(error);
throw new Error(`jupSOL staking failed: ${error.message}`);
}
}

View File

@@ -19,8 +19,6 @@ export async function trade(
slippageBps: number = DEFAULT_OPTIONS.SLIPPAGE_BPS,
): Promise<string> {
try {
// Get quote for the swap
console.log(inputMint.toString(), outputMint.toString(), inputAmount, slippageBps);
const quoteResponse = await (
await fetch(
`${JUP_API}/quote?` +

View File

@@ -39,7 +39,6 @@ export interface PumpfunLaunchResponse {
error?: string;
}
/**
* Lulo Account Details response format
*/
@@ -54,3 +53,27 @@ export interface LuloAccountDetailsResponse {
minimumRate: string;
};
}
export interface JupiterTokenData {
address: string;
name: string;
symbol: string;
decimals: number;
tags: string[];
logoURI: string;
daily_volume: number;
freeze_authority: string | null;
mint_authority: string | null;
permanent_delegate: string | null;
extensions: {
coingeckoId?: string;
};
}
export interface FetchPriceResponse {
status: "success" | "error";
tokenId?: string;
priceInUSDC?: string;
message?: string;
code?: string;
}

View File

@@ -41,7 +41,7 @@ export async function getPriorityFees(connection: Connection): Promise<{
const median =
sortedFees.length % 2 === 0
? ((sortedFees[mid - 1] ?? 0) + (sortedFees[mid] ?? 0)) / 2
: (sortedFees[mid] ?? 0);
: sortedFees[mid] ?? 0;
// Helper to create priority fee IX based on chosen strategy
const createPriorityFeeIx = (fee: number) => {
@@ -76,7 +76,7 @@ export async function getPriorityFees(connection: Connection): Promise<{
export async function sendTx(
agent: SolanaAgentKit,
tx: Transaction,
otherKeypairs?: Keypair[],
otherKeypairs?: Keypair[]
) {
tx.recentBlockhash = (await agent.connection.getLatestBlockhash()).blockhash;
tx.feePayer = agent.wallet_address;
@@ -90,8 +90,9 @@ export async function sendTx(
await agent.connection.confirmTransaction({
signature: txid,
blockhash: (await agent.connection.getLatestBlockhash()).blockhash,
lastValidBlockHeight: (await agent.connection.getLatestBlockhash())
.lastValidBlockHeight,
lastValidBlockHeight: (
await agent.connection.getLatestBlockhash()
).lastValidBlockHeight,
});
return txid;
}