diff --git a/src/agent/index.ts b/src/agent/index.ts index 6ebc6bd3..0cba7a2c 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -76,6 +76,10 @@ import { multisig_reject_proposal, multisig_approve_proposal, multisig_execute_proposal, + verifyProgram, + checkVerificationStatus, + cancelVerification, + VerificationOptions, } from "../tools"; import { Config, @@ -654,4 +658,36 @@ export class SolanaAgentKit { ): Promise { return multisig_execute_proposal(this, transactionIndex); } + + async verifyProgram( + programId: string, + repository: string, + commitHash: string, + options?: VerificationOptions, + ) { + const processedOptions: VerificationOptions | undefined = options + ? { + libName: options.libName ?? null, + bpfFlag: options.bpfFlag ?? null, + cargoArgs: options.cargoArgs ?? null, + verifyProgramId: options.verifyProgramId ?? null, + } + : undefined; + + return verifyProgram( + this, + programId, + repository, + commitHash, + processedOptions, + ); + } + + async checkVerificationStatus(programId: string) { + return checkVerificationStatus(programId); + } + + async cancelVerification(programId: PublicKey, verifyProgramId?: PublicKey) { + return cancelVerification(this, programId, verifyProgramId); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 707a5d22..a5000f6b 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -24,6 +24,7 @@ export * from "./tiplink"; export * from "./sns"; export * from "./lightprotocol"; export * from "./squads"; +export * from "./verification"; import { SolanaAgentKit } from "../agent"; import { diff --git a/src/langchain/verification/index.ts b/src/langchain/verification/index.ts new file mode 100644 index 00000000..307c2efe --- /dev/null +++ b/src/langchain/verification/index.ts @@ -0,0 +1 @@ +export * from "./verify_program"; diff --git a/src/langchain/verification/verify_program.ts b/src/langchain/verification/verify_program.ts new file mode 100644 index 00000000..af0df517 --- /dev/null +++ b/src/langchain/verification/verify_program.ts @@ -0,0 +1,164 @@ +import { PublicKey } from "@solana/web3.js"; +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export interface VerificationOptions { + verifyProgramId: PublicKey | null; + libName: string | null; + bpfFlag: boolean | null; + cargoArgs: string[] | null; +} + +export interface VerificationResponse { + verificationPda: string; + status: "success" | "error"; + message: string; + jobId?: string; +} + +export interface VerificationInput { + programId: string; + repository: string; + commitHash: string; + verifyProgramId?: string; + libName?: string; + bpfFlag?: boolean; + cargoArgs?: string[]; +} + +interface StatusCheckInput { + programId: string; +} + +interface CancellationInput { + programId: string; + verifyProgramId?: string; +} + +export class SolanaVerifyTool extends Tool { + name = "solana_program_verification"; + description = `Verify a Solana program using its source code repository. + Input is a JSON string with: + - programId: string (required) - Solana program ID to verify + - repository: string (required) - GitHub repository URL + - commitHash: string (required) - Git commit hash or branch name + - verifyProgramId: string (optional) - Custom verify program ID + - libName: string (optional) - Library name for multi-program repos + - bpfFlag: boolean (optional) - Use cargo build-bpf instead of build-sbf + - cargoArgs: string[] (optional) - Additional cargo build arguments`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput: VerificationInput = JSON.parse(input); + + this.validateInput(parsedInput); + + const options: VerificationOptions = { + verifyProgramId: parsedInput.verifyProgramId + ? new PublicKey(parsedInput.verifyProgramId) + : null, + libName: parsedInput.libName ?? null, + bpfFlag: parsedInput.bpfFlag ?? null, + cargoArgs: parsedInput.cargoArgs ?? null, + }; + + const result = await this.solanaKit.verifyProgram( + parsedInput.programId, + parsedInput.repository, + parsedInput.commitHash, + options, + ); + + return this.formatSuccessResponse(result); + } catch (error) { + return this.formatErrorResponse(error); + } + } + + async cancelVerification(input: string): Promise { + try { + const parsedInput: CancellationInput = JSON.parse(input); + + if (!parsedInput.programId) { + throw new Error("Program ID is required"); + } + + const programId = new PublicKey(parsedInput.programId); + const verifyProgramId = parsedInput.verifyProgramId + ? new PublicKey(parsedInput.verifyProgramId) + : undefined; + + const signature = await this.solanaKit.cancelVerification( + programId, + verifyProgramId, + ); + + return JSON.stringify({ + status: "success", + message: "Verification cancelled successfully", + signature, + }); + } catch (error) { + return this.formatErrorResponse(error); + } + } + + async checkStatus(input: string): Promise { + try { + const parsedInput: StatusCheckInput = JSON.parse(input); + + if (!parsedInput.programId) { + throw new Error("Program ID is required"); + } + + const status = await this.solanaKit.checkVerificationStatus( + parsedInput.programId, + ); + + return JSON.stringify({ + status: "success", + ...status, + }); + } catch (error) { + return this.formatErrorResponse(error); + } + } + + private validateInput(input: VerificationInput): void { + if (!input.programId) { + throw new Error("Program ID is required"); + } + if (!input.repository) { + throw new Error("Repository URL is required"); + } + if (!input.commitHash) { + throw new Error("Commit hash is required"); + } + } + + private formatSuccessResponse(result: VerificationResponse): string { + return JSON.stringify({ + status: "success", + verificationPda: result.verificationPda, + message: result.message, + jobId: result.jobId, + }); + } + + private formatErrorResponse(error: unknown): string { + const errorMessage = error instanceof Error ? error.message : String(error); + + return JSON.stringify({ + status: "error", + message: errorMessage, + code: + error instanceof Error && "code" in error + ? (error as any).code + : "UNKNOWN_ERROR", + }); + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 6a546e9b..b403199e 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -23,3 +23,4 @@ export * from "./3land"; export * from "./tiplink"; export * from "./lightprotocol"; export * from "./squads"; +export * from "./verification"; diff --git a/src/tools/verification/index.ts b/src/tools/verification/index.ts new file mode 100644 index 00000000..307c2efe --- /dev/null +++ b/src/tools/verification/index.ts @@ -0,0 +1 @@ +export * from "./verify_program"; diff --git a/src/tools/verification/verify_program.ts b/src/tools/verification/verify_program.ts new file mode 100644 index 00000000..c472159f --- /dev/null +++ b/src/tools/verification/verify_program.ts @@ -0,0 +1,207 @@ +import { + PublicKey, + Transaction, + TransactionInstruction, + SystemProgram, +} from "@solana/web3.js"; +import { SolanaAgentKit } from "../../index"; +import { BN } from "@coral-xyz/anchor"; +import axios from "axios"; + +export interface VerificationOptions { + libName?: string | null; + bpfFlag?: boolean | null; + cargoArgs?: string[] | null; + verifyProgramId?: PublicKey | null; +} + +export interface VerificationResponse { + verificationPda: string; + status: "success" | "error"; + message: string; + jobId?: string; +} + +/** + * Verifies a Solana program by signing a PDA and submitting to verification + * @param agent SolanaAgentKit instance + * @param programId Program ID to verify + * @param repository GitHub repository URL + * @param commitHash Git commit hash or branch + * @param options Additional verification options including custom verify program ID + * @returns Object containing verification PDA address and status + */ +export async function verifyProgram( + agent: SolanaAgentKit, + programId: string, + repository: string, + commitHash: string, + options?: VerificationOptions, +): Promise { + try { + // Validate program ID + const programPubkey = new PublicKey(programId); + if (!PublicKey.isOnCurve(programPubkey)) { + throw new Error("Invalid program ID"); + } + + // Check if program exists + const programInfo = await agent.connection.getAccountInfo(programPubkey); + if (!programInfo) { + throw new Error(`Program ${programId} does not exist`); + } + + // Use provided verify program ID or default to OtterSec + const verifyProgramId = + options?.verifyProgramId || + new PublicKey("vfPD3zB5TipA61PJq5qWQwJqg4mZpkZwA4Z1YFRHy6m"); + + // Find PDA for verification + const [verificationPda, bump] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("verification"), + programPubkey.toBuffer(), + agent.wallet_address.toBuffer(), + ], + verifyProgramId, + ); + + // Create verification data + const verificationData = { + repository, + commitHash, + buildArgs: options?.cargoArgs || [], + buildEnv: options?.bpfFlag ? "bpf" : "sbf", + verifier: agent.wallet_address.toString(), + }; + + // Create verification instruction + const instruction = new TransactionInstruction({ + keys: [ + { pubkey: agent.wallet_address, isSigner: true, isWritable: true }, + { pubkey: verificationPda, isSigner: false, isWritable: true }, + { pubkey: programPubkey, isSigner: false, isWritable: false }, + { pubkey: agent.wallet_address, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + programId: verifyProgramId, + data: Buffer.concat([ + Buffer.from([0]), // Instruction discriminator for verify + Buffer.from(JSON.stringify(verificationData)), + new BN(bump).toArrayLike(Buffer, "le", 1), + ]), + }); + + // Sign and send verification transaction + const transaction = new Transaction().add(instruction); + const signature = await agent.connection.sendTransaction( + transaction, + [agent.wallet], + { preflightCommitment: "confirmed" }, + ); + await agent.connection.confirmTransaction(signature); + + // Submit verification job + const verifyResponse = await axios.post("https://verify.osec.io/verify", { + program_id: programId, + repository, + commit_hash: commitHash, + lib_name: options?.libName, + bpf_flag: options?.bpfFlag, + cargo_args: options?.cargoArgs, + }); + + const jobId = verifyResponse.data.job_id; + + // Monitor verification status + let attempts = 0; + while (attempts < 30) { + // 5 min timeout (10s intervals) + const status = await axios.get(`https://verify.osec.io/jobs/${jobId}`); + + if (status.data.status === "completed") { + return { + status: "success", + message: "Program verification successful", + verificationPda: verificationPda.toString(), + jobId, + }; + } else if (status.data.status === "failed") { + throw new Error( + `Verification failed: ${status.data.error || "Unknown error"}`, + ); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + attempts++; + } + + throw new Error("Verification timed out"); + } catch (error: any) { + console.error("Full error details:", error); + throw error; + } +} + +/** + * Cancel an existing program verification + * @param agent SolanaAgentKit instance + * @param programId Program ID to cancel verification + * @param verifyProgramId Optional custom verify program ID + * @returns Transaction signature + */ +export async function cancelVerification( + agent: SolanaAgentKit, + programId: PublicKey, + verifyProgramId?: PublicKey, +): Promise { + // Use provided verify program ID or default + const verificationProgramId = + verifyProgramId || + new PublicKey("vfPD3zB5TipA61PJq5qWQwJqg4mZpkZwA4Z1YFRHy6m"); + + // Find the verification PDA + const [verificationPda] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("verification"), + programId.toBuffer(), + agent.wallet_address.toBuffer(), + ], + verificationProgramId, + ); + + // Create cancel instruction + const instruction = new TransactionInstruction({ + keys: [ + { pubkey: agent.wallet_address, isSigner: true, isWritable: true }, + { pubkey: verificationPda, isSigner: false, isWritable: true }, + { pubkey: programId, isSigner: false, isWritable: false }, + ], + programId: verificationProgramId, + data: Buffer.from([1]), + }); + + // Send transaction + const transaction = new Transaction().add(instruction); + return await agent.connection.sendTransaction(transaction, [agent.wallet]); +} + +/** + * Check verification status for a program + * @param programId Program ID to check + * @returns Verification status from API + */ +export async function checkVerificationStatus(programId: string): Promise<{ + is_verified: boolean; + message: string; + on_chain_hash: string; + executable_hash: string; + repo_url: string; + commit: string; + last_verified_at: string | null; +}> { + const response = await axios.get( + `https://verify.osec.io/status/${programId}`, + ); + return response.data; +}