wip airship

This commit is contained in:
Swenschaeferjohann
2024-12-20 05:04:43 +00:00
parent 30c534c54c
commit e0ff4399df
9 changed files with 1518 additions and 140 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

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

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

View File

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

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