Merge branch 'main' into calintje/main

This commit is contained in:
aryan
2024-12-20 19:16:08 +05:30
37 changed files with 1798 additions and 568 deletions

View File

@@ -243,7 +243,7 @@ export async function createOrcaSingleSidedWhirlpool(
const tickUpperInitializableIndex = TickUtil.getInitializableTickIndex(tickUpperIndex, tickSpacing);
if (!TickUtil.checkTickInBounds(tickLowerInitializableIndex) || !TickUtil.checkTickInBounds(tickUpperInitializableIndex)) throw Error('Prices out of bounds');
const increasLiquidityQuoteParam: IncreaseLiquidityQuoteParam = {
inputTokenAmount: BN(depositTokenAmount),
inputTokenAmount: new BN(depositTokenAmount),
inputTokenMint: depositTokenMint,
tokenMintA: mintA,
tokenMintB: mintB,

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,57 +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 new token mint
const lamports = await getMinimumBalanceForRentExemptAccount(
agent.connection
);
// Create UMI instance from agent
const umi = createUmi(agent.connection.rpcEndpoint)
umi.use(keypairIdentity(fromWeb3JsKeypair(agent.wallet)));
const mint = Keypair.generate();
let account_create_ix = SystemProgram.createAccount({
fromPubkey: agent.wallet_address,
newAccountPubkey: mint.publicKey,
lamports,
space: MINT_SIZE,
programId: TOKEN_PROGRAM_ID,
// Create new token mint
const mint = generateSigner(umi);
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]);
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}`);
}
}

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,9 +6,14 @@ 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 './stake_with_jup';
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";

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