mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-13 23:16:55 +00:00
wip airship
This commit is contained in:
@@ -26,15 +26,21 @@
|
||||
"@metaplex-foundation/umi-web3js-adapters": "^0.9.2",
|
||||
"@solana/spl-token": "^0.4.9",
|
||||
"@solana/web3.js": "^1.95.4",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"bs58": "^6.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.38.2",
|
||||
"form-data": "^4.0.1",
|
||||
"helius-airship-core": "file:../airship/packages/core",
|
||||
"langchain": "^0.3.6",
|
||||
"openai": "^4.75.0",
|
||||
"sqlocal": "^0.13.0",
|
||||
"typedoc": "^0.26.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/node": "^22.9.0",
|
||||
"ts-node": "^10.9.2"
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
1145
pnpm-lock.yaml
generated
1145
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,8 @@ import {
|
||||
getTokenDataByAddress,
|
||||
getTokenDataByTicker,
|
||||
stakeWithJup,
|
||||
createCompressedAirdrop,
|
||||
sendCompressedAirdrop,
|
||||
} from "../tools";
|
||||
import { CollectionOptions, PumpFunTokenOptions } from "../types";
|
||||
import { DEFAULT_OPTIONS } from "../constants";
|
||||
@@ -57,7 +59,7 @@ export class SolanaAgentKit {
|
||||
uri: string,
|
||||
symbol: string,
|
||||
decimals: number = DEFAULT_OPTIONS.TOKEN_DECIMALS,
|
||||
initialSupply?: number,
|
||||
initialSupply?: number
|
||||
) {
|
||||
return deploy_token(this, name, uri, symbol, decimals, initialSupply);
|
||||
}
|
||||
@@ -87,11 +89,11 @@ export class SolanaAgentKit {
|
||||
}
|
||||
|
||||
async resolveSolDomain(domain: string) {
|
||||
return resolveSolDomain(this, domain)
|
||||
return resolveSolDomain(this, domain);
|
||||
}
|
||||
|
||||
async getPrimaryDomain(account: PublicKey) {
|
||||
return getPrimaryDomain(this, account)
|
||||
return getPrimaryDomain(this, account);
|
||||
}
|
||||
|
||||
async trade(
|
||||
@@ -136,9 +138,21 @@ export class SolanaAgentKit {
|
||||
);
|
||||
}
|
||||
|
||||
async stake(
|
||||
amount: number,
|
||||
) {
|
||||
async stake(amount: number) {
|
||||
return stakeWithJup(this, amount);
|
||||
}
|
||||
|
||||
async airdropCompressedTokens(
|
||||
mintAddress: string,
|
||||
amount: number,
|
||||
recipients: string[]
|
||||
) {
|
||||
await createCompressedAirdrop(
|
||||
this,
|
||||
new PublicKey(mintAddress),
|
||||
BigInt(amount),
|
||||
recipients.map((recipient) => new PublicKey(recipient))
|
||||
);
|
||||
return await sendCompressedAirdrop(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,6 @@ export class SolanaMintNFTTool extends Tool {
|
||||
try {
|
||||
const parsedInput = JSON.parse(input);
|
||||
|
||||
|
||||
const result = await this.solanaKit.mintNFT(
|
||||
new PublicKey(parsedInput.collectionMint),
|
||||
{
|
||||
@@ -703,6 +702,46 @@ export class SolanaTokenDataByTickerTool extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaAirdropCompressedTokensTool extends Tool {
|
||||
name = "solana_airdrop_compressed_tokens";
|
||||
description = `Airdrop tokens with zk compression
|
||||
|
||||
Inputs:
|
||||
- mintAddress: string, the mint address of the token, e.g., "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"
|
||||
- amount: number, the amount of tokens to airdrop per recipient, e.g., 42
|
||||
- recipients: string[], the recipient addresses, e.g., ["JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"]
|
||||
`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const parsedInput = JSON.parse(input);
|
||||
if (parsedInput.recipients.length <= 100) {
|
||||
throw new Error("Recipients array must contain at least 420 addresses");
|
||||
}
|
||||
await this.solanaKit.airdropCompressedTokens(
|
||||
parsedInput.mintAddress,
|
||||
parsedInput.amount,
|
||||
parsedInput.recipients
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: `Airdropped ${parsedInput.amount} tokens to ${parsedInput.recipients.length} recipients.`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createSolanaTools(solanaKit: SolanaAgentKit) {
|
||||
return [
|
||||
new SolanaBalanceTool(solanaKit),
|
||||
@@ -724,5 +763,6 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
|
||||
new SolanaGetDomainTool(solanaKit),
|
||||
new SolanaTokenDataTool(solanaKit),
|
||||
new SolanaTokenDataByTickerTool(solanaKit),
|
||||
new SolanaAirdropCompressedTokensTool(solanaKit),
|
||||
];
|
||||
}
|
||||
|
||||
228
src/tools/airdrop_compressed_tokens/index.ts
Normal file
228
src/tools/airdrop_compressed_tokens/index.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import type { DrizzleDb, WorkerMessage, WorkerData } from "./types";
|
||||
import { SolanaAgentKit } from "../../agent/index.js";
|
||||
|
||||
let db: DrizzleDb | null = null;
|
||||
let dbInitPromise: Promise<DrizzleDb | null> | null = null;
|
||||
|
||||
async function configureForBrowser() {
|
||||
try {
|
||||
const [{ SQLocalDrizzle }, { drizzle }, { sql }, heliusCore] =
|
||||
await Promise.all([
|
||||
// @ts-ignore
|
||||
import("sqlocal/drizzle"),
|
||||
import("drizzle-orm/sqlite-proxy"),
|
||||
import("drizzle-orm"),
|
||||
import("helius-airship-core"),
|
||||
]);
|
||||
|
||||
const { databaseFile } = heliusCore;
|
||||
|
||||
const { driver, batchDriver } = new SQLocalDrizzle({
|
||||
databasePath: databaseFile,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
const database = drizzle(driver, batchDriver);
|
||||
await database.run(sql`PRAGMA journal_mode = WAL;`);
|
||||
await database.run(sql`PRAGMA synchronous = normal;`);
|
||||
|
||||
return database;
|
||||
} catch (error) {
|
||||
console.error("Browser database configuration failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function configureForNode() {
|
||||
try {
|
||||
const [{ drizzle }, { default: Database }, { databaseFile }] =
|
||||
await Promise.all([
|
||||
import("drizzle-orm/better-sqlite3"),
|
||||
import("better-sqlite3"),
|
||||
import("helius-airship-core"),
|
||||
]);
|
||||
|
||||
const sqlite = new Database(databaseFile);
|
||||
sqlite.exec("PRAGMA journal_mode = WAL;");
|
||||
sqlite.exec("PRAGMA synchronous = normal;");
|
||||
|
||||
return drizzle(sqlite);
|
||||
} catch (error) {
|
||||
console.error("Node database configuration failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function configureDatabase(): Promise<DrizzleDb> {
|
||||
if (!dbInitPromise) {
|
||||
dbInitPromise = (async () => {
|
||||
if (!db) {
|
||||
db =
|
||||
typeof window !== "undefined"
|
||||
? await configureForBrowser()
|
||||
: await configureForNode();
|
||||
}
|
||||
return db;
|
||||
})();
|
||||
}
|
||||
|
||||
const database = await dbInitPromise;
|
||||
if (!database) throw new Error("Database initialization failed");
|
||||
return database;
|
||||
}
|
||||
async function createWorker(): Promise<
|
||||
Worker | import("worker_threads").Worker
|
||||
> {
|
||||
if (typeof window !== "undefined") {
|
||||
const origin = new URL(window.location.href).origin;
|
||||
if (
|
||||
!origin.startsWith("https://") &&
|
||||
!origin.startsWith("http://localhost")
|
||||
) {
|
||||
throw new Error("Invalid origin protocol");
|
||||
}
|
||||
|
||||
const workerCode = `
|
||||
self.importScripts('${origin}/airdrop-worker.js'.replace(/[<>'"]/g, ''));
|
||||
`;
|
||||
|
||||
const blobOptions = {
|
||||
type: "application/javascript",
|
||||
headers: {
|
||||
"Content-Security-Policy": "default-src 'self'",
|
||||
},
|
||||
};
|
||||
|
||||
const blob = new Blob([workerCode], blobOptions);
|
||||
const workerUrl = URL.createObjectURL(blob);
|
||||
const worker = new Worker(workerUrl);
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
return worker;
|
||||
} else {
|
||||
// Node
|
||||
const { Worker } = await import("worker_threads");
|
||||
const path = await import("path");
|
||||
|
||||
return new Worker(path.resolve(__dirname, "worker.js"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create airdrop with zk compression
|
||||
* @param agent Agent
|
||||
* @param mintAddress Token mint public key (non token-2022)
|
||||
* @param amount amount of tokens to airdrop per recipient
|
||||
* @param recipients Recipient public keys
|
||||
*/
|
||||
export async function createCompressedAirdrop(
|
||||
agent: SolanaAgentKit,
|
||||
mintAddress: PublicKey,
|
||||
amount: bigint,
|
||||
recipients: PublicKey[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const database = await configureDatabase();
|
||||
const { create, init } = await import("helius-airship-core");
|
||||
|
||||
await init({
|
||||
db: database as any,
|
||||
});
|
||||
|
||||
await create({
|
||||
db: database as any,
|
||||
signer: agent.wallet.publicKey,
|
||||
addresses: recipients,
|
||||
amount,
|
||||
mintAddress,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Create operation failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send airdrop. must be called after `createCompressedAirdrop`
|
||||
* @param agent Agent
|
||||
* @param onProgress Callback for progress updates
|
||||
* @param onError Callback for error handling
|
||||
*/
|
||||
export async function sendCompressedAirdrop(
|
||||
agent: SolanaAgentKit,
|
||||
onProgress?: (progress: number) => void,
|
||||
onError?: (error: Error) => void
|
||||
): Promise<void> {
|
||||
let worker: Worker | import("worker_threads").Worker | null = null;
|
||||
const { databaseFile } = await import("helius-airship-core");
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
if (worker) {
|
||||
try {
|
||||
if ("terminate" in worker) {
|
||||
worker.terminate();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Main] Worker cleanup error:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
worker = await createWorker();
|
||||
|
||||
const message: WorkerData = {
|
||||
type: "send",
|
||||
data: {
|
||||
secretKey: Array.from(agent.wallet.secretKey),
|
||||
url: agent.connection.rpcEndpoint,
|
||||
dbPath: databaseFile,
|
||||
},
|
||||
};
|
||||
|
||||
const handleMessage = (event: MessageEvent<WorkerMessage>) => {
|
||||
if (event.data === undefined) {
|
||||
cleanup();
|
||||
reject(new Error());
|
||||
return;
|
||||
}
|
||||
const { type, data, error } = event.data;
|
||||
switch (type) {
|
||||
case "progress":
|
||||
onProgress?.(data!);
|
||||
break;
|
||||
case "error":
|
||||
cleanup();
|
||||
const errorObj = new Error(error);
|
||||
onError?.(errorObj);
|
||||
reject(errorObj);
|
||||
break;
|
||||
case "complete":
|
||||
cleanup();
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
(worker as Worker).onmessage = handleMessage;
|
||||
} else {
|
||||
(worker as import("worker_threads").Worker).on(
|
||||
"message",
|
||||
handleMessage
|
||||
);
|
||||
}
|
||||
|
||||
worker.postMessage(message);
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
console.error("[Main] Send operation failed:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
onError?.(error instanceof Error ? error : new Error(String(error)));
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
69
src/tools/airdrop_compressed_tokens/types.ts
Normal file
69
src/tools/airdrop_compressed_tokens/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
||||
import type { SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy";
|
||||
|
||||
export interface Closeable {
|
||||
close(): Promise<void> | void;
|
||||
}
|
||||
|
||||
export type DrizzleDb = BetterSQLite3Database | SqliteRemoteDatabase;
|
||||
|
||||
export interface WorkerMessage {
|
||||
type: "progress" | "error" | "complete";
|
||||
data?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface WorkerData {
|
||||
type: "send" | "poll";
|
||||
data: {
|
||||
dbPath: string;
|
||||
url: string;
|
||||
secretKey?: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkerError extends Error {
|
||||
code?: string;
|
||||
type: "worker_error";
|
||||
}
|
||||
|
||||
export interface DatabaseError extends Error {
|
||||
code?: string;
|
||||
type: "database_error";
|
||||
}
|
||||
|
||||
export function isWorkerMessage(message: any): message is WorkerMessage {
|
||||
return (
|
||||
message &&
|
||||
typeof message === "object" &&
|
||||
"type" in message &&
|
||||
(message.type === "progress" ||
|
||||
message.type === "error" ||
|
||||
message.type === "complete")
|
||||
);
|
||||
}
|
||||
|
||||
export function isWorkerData(data: any): data is WorkerData {
|
||||
return (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"type" in data &&
|
||||
(data.type === "send" || data.type === "poll") &&
|
||||
"data" in data &&
|
||||
typeof data.data === "object" &&
|
||||
typeof data.data.dbPath === "string" &&
|
||||
typeof data.data.url === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isWorkerError(error: any): error is WorkerError {
|
||||
return (
|
||||
error instanceof Error && "type" in error && error.type === "worker_error"
|
||||
);
|
||||
}
|
||||
|
||||
export function isDatabaseError(error: any): error is DatabaseError {
|
||||
return (
|
||||
error instanceof Error && "type" in error && error.type === "database_error"
|
||||
);
|
||||
}
|
||||
128
src/tools/airdrop_compressed_tokens/worker.ts
Normal file
128
src/tools/airdrop_compressed_tokens/worker.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { send } from "helius-airship-core";
|
||||
import { Keypair } from "@solana/web3.js";
|
||||
import type { WorkerMessage, WorkerData, DrizzleDb, Closeable } from "./types";
|
||||
|
||||
let db: DrizzleDb | null = null;
|
||||
let dbInitPromise: Promise<DrizzleDb | null> | null = null;
|
||||
|
||||
async function initializeDb(dbPath: string): Promise<DrizzleDb> {
|
||||
if (!dbInitPromise) {
|
||||
dbInitPromise = (async () => {
|
||||
if (!db) {
|
||||
try {
|
||||
if (typeof window !== "undefined") {
|
||||
const [{ SQLocalDrizzle }, { drizzle }, { sql }] =
|
||||
await Promise.all([
|
||||
// @ts-ignore
|
||||
import("sqlocal/drizzle"),
|
||||
import("drizzle-orm/sqlite-proxy"),
|
||||
import("drizzle-orm"),
|
||||
]);
|
||||
|
||||
const { driver, batchDriver } = new SQLocalDrizzle({
|
||||
databasePath: dbPath,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
db = drizzle(driver, batchDriver);
|
||||
await db.run(sql`PRAGMA journal_mode = WAL;`);
|
||||
await db.run(sql`PRAGMA synchronous = normal;`);
|
||||
} else {
|
||||
const [{ drizzle }, { default: Database }] = await Promise.all([
|
||||
import("drizzle-orm/better-sqlite3"),
|
||||
import("better-sqlite3"),
|
||||
]);
|
||||
|
||||
const sqlite = new Database(dbPath);
|
||||
sqlite.exec("PRAGMA journal_mode = WAL;");
|
||||
sqlite.exec("PRAGMA synchronous = normal;");
|
||||
db = drizzle(sqlite);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Worker database initialization failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return db;
|
||||
})();
|
||||
}
|
||||
|
||||
const database = await dbInitPromise;
|
||||
if (!database) throw new Error("Worker database initialization failed");
|
||||
return database;
|
||||
}
|
||||
|
||||
function postMessage(message: WorkerMessage) {
|
||||
if (typeof window !== "undefined") {
|
||||
self.postMessage(message);
|
||||
} else {
|
||||
const { parentPort } = require("worker_threads");
|
||||
parentPort?.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessage(data: WorkerData) {
|
||||
let database: DrizzleDb | null = null;
|
||||
|
||||
try {
|
||||
database = await initializeDb(data.data.dbPath);
|
||||
|
||||
switch (data.type) {
|
||||
case "send":
|
||||
if (!data.data.secretKey) {
|
||||
throw new Error("Secret key is required for send operation");
|
||||
}
|
||||
|
||||
await send({
|
||||
db: database,
|
||||
keypair: Keypair.fromSecretKey(new Uint8Array(data.data.secretKey)),
|
||||
url: data.data.url,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
postMessage({ type: "complete" });
|
||||
} catch (error) {
|
||||
console.error("[Worker] Operation failed:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
type: data.type,
|
||||
url: data.data.url,
|
||||
});
|
||||
|
||||
postMessage({
|
||||
type: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
if (database && "close" in database) {
|
||||
try {
|
||||
await (database as Closeable).close();
|
||||
} catch (error) {
|
||||
console.error("Error closing database connection:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
self.onmessage = (event: MessageEvent<WorkerData>) => {
|
||||
handleMessage(event.data).catch((error) => {
|
||||
postMessage({
|
||||
type: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
};
|
||||
} else {
|
||||
const { parentPort } = require("worker_threads");
|
||||
parentPort?.on("message", (data: WorkerData) => {
|
||||
handleMessage(data).catch((error) => {
|
||||
postMessage({
|
||||
type: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,5 +12,6 @@ export * from "./launch_pumpfun_token";
|
||||
export * from "./lend";
|
||||
export * from "./get_tps";
|
||||
export * from "./get_token_data";
|
||||
export * from './stake_with_jup';
|
||||
export * from "./stake_with_jup";
|
||||
export * from "./fetch_price";
|
||||
export * from "./airdrop_compressed_tokens";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user